Page 2 of 2

Re: KnobGadget() - Rev1.02 Feedback please!

Posted: Fri Mar 11, 2016 6:03 pm
by RichardL
Hi fellow knob twiddlers,
I have been using this for a long time and decided to dust it down and address some of the issues that have come to the surface in recent years. Here is version 2.0... the changes are in the Release Info section at the start of the file. I would be keen to hear of any problems... or otherwise!
RichardL

Code: Select all

; ================================
; Name:    KnobGadget
; Version: 2.0
; Author:  RichardL
; Date:    22nd Feb 2016
; OS:      Windows
; PB ver.: 5.41
; License: Free
; ====================================

;{ Release info...
; 0.90 First release version

; 0.91 Added poor man's anti-aliasing to backdrop
;      Improved Font specification
;      Improved radial markers... rather nice!
;      Added flag bit to control 'heat ring'
;      Demo includes slaving two knobs.

; 0.92 4th June 2013 
;      Added 'jazzy' knob top with control bit in flag
;      Added frame option with control bit in flag
;      Added frame border options
;      Improved scaling and caption positioning4

; 1.00 5th June 2013
;      Modified control range. Knob 300 degrees = 16383 'clicks'
;      Demo shows how to apply small steps with MouseWheel
;        Mouse wheel UP   - Fine adjust 1 clicks per mouse increment
;        Mouse wheel DOWN - COARSE adjust 20 clicks per mouse increment

;      Now code is an IncludeFile() (=.PBI) but the demo code is automatically
;      compiled if this file is compiled on its own for testing, thanks to
;                 <<< CompilerIf #PB_Compiler_IsMainFile >>>

; 1.01 23rd June 2013 (505)
;      Fixed benign bug with \KnobRate
;      Added flag bit and code to create a borderless version
;      Modified demo to show all flag options
;      Code version 05 as I'm changing the input syntax and other things 
;      to allow Max and Min like a ScrollBar().
;      Added #MultiTurn1 flag. Knob goes round with mouse but 'span'
;      is spread over 'N' revolutions.

; 1.02 8th July 2013 (542)
;     Added #SWITCHKNOB - Knob clicks to one of 'N' positions.
;     Added #PLAINKKNOB - No Graduations or RING_Heat... good for SWITCH version.

; 1.03 
;     *** WARNING *** CHANGE MAY BE REQUIRED TO USER CODE
;     Changed 'wParam' value sent to callback, it was a WORD giving the knob's position.
;     It is now the index into knob's data. Get knob position with Knob(wParam)\Position. 
;     Modification allows MIN, MAX and returned values to be INTEGER values.

;     Added option to show a LED indicator top left and switch it On when value
;     exceeds knob range / 64. State of LED reported by flag bit. Bit UNTIDY at present.

; 2.0 Major hack. 
; DONE Now uses BindGadgetEvent() to invoke servicing of Gadget,
;      so removed local event manager for dragging knob.... now more system friendly.
;      Now only reports when knob changes.

; DONE New feature. Knobs now report different values if SHIFT is pressed when selected.
;      Check for shift status knob is using by checking \ShiftFlag for selected knob.    
;      Knob position is in \Position and \PositionShift. 
; DONE  C/CanvasID/CanvasNum/A to avoid confusion with PB conventions for 'ID'
; DONE  Added syntax info to KnobGaqdget()
; DONE  Define rates for mouse wheel up / down for each Knob(). See  SetKnobWheelRates(SloR.i,FstR.i)
; DONE  Reduce Knob() creep when selected many times... 
; TODO  EnableExplicit ... Soon folks!
;       Caption update so knob value can be shown... in user units.


; BUGS - SetKnobState() sometimes wrong on multi-turn pots... I think.

;}
;{ Useful references
; http://msdn.microsoft.com/en-us/library/windows/desktop/ms644931(v=vs.85).aspx
; http://www.purebasic.fr/english/viewtopic.php?f=12&T=54117&p=409390&hilit=Meter+Gadget#p409390
;}
;{ Procedure declarations
Declare SetKnobState(gadNum,Value.f=0) 
Declare GetKnobState(gadNum)
Declare SetKnobWheelRates(SloR.i,FstR.i)
;}
;{ Structures, constants etc
;- KnobGadget() types and option flags
EnumerationBinary
  
  ; Knob types... call KnobGadget() with Flags including just ONE of these.
  #POTKNOB  
  #MULTITURN1 
  #MULTITURN2 
  #SWITCHKNOB
  #PLAINKKNOB 
  
  ; Additional flags to control optional features...
  #RING_Heat    
  #FRAME_Switch
  #JAZZ_top
  #BORDER
  #LED_Switch
  
EndEnumeration

; Where N is the 'gearing rate'
; ** MULTITURN1 - Knob tracks mouse and value increase by 1/Number of turns
;    MULTITURN2 - Knob rotates at Rate/N

Structure KNOBTYPE
  width.i               ; Knob gadget width - INSIDE FRAME
  height.i              ; knob height
  KnobImage.i           ; Image that has the knob background.. NOT the pointer
  CanvasNum.i           ; Gadget number
  CentreX.i             ; Knob co-ords within the gadget
  CentreY.i
  BackColour.i  
  FrameColourInner.i
  FrameColourOuter.i
  KnobRad.i             ; Radius of knob
  KnobColour.i
  ScaleColour.i         ; Radial lines around knob
  Caption.s             ; Knob caption
  CaptionColour.i       ; and the colour
  DotColour.i           ; The 'pointer' dot and line on top of knob
  
  LastDotX.i            ; Position of dot. SHIFT key up
  LastDotY.i
  
  LastShiftDotX.i       ; Position of dot. SHIFT key down 
  LastShiftDotY.i
  
  Position.f            ; Knob value, returned with GetKnobState(GadNum) or via callback
  PositionShift.f
  ShiftFlag.i
  
  Gate.i
  KnobMin.i
  KnobMax.i
  KnobClick.i
  KnobRate.f
  KnobTurns.i
  LastF.f
  flags.i               ; Bitwise option flags...
  CaptionFont.s         ; Use this font for knob caption.
  CaptionFontHt.i       ; with this height.
  WheelRateSlow.i       ; Knob delta per wheel click - Wheel up.
  WheelRateFast.i       ; Knob delta per wheel click - Wheel down.
EndStructure

#PBM_KNOB = #WM_APP + 1 ; Application specific callback value
#Scale = 3              ; Scaling factor for the 'poor man's anti-aliasing)

Global KnobDefaults.KNOBTYPE ; KnobDefaults
Global Dim Knob.KNOBTYPE(1)
Global NewList KnobList.i()

; Default knob parameters... 
; SOME may be adjusted BEFORE a knob is created.

With KnobDefaults
  ; These values can be programmed directly or with the procedures provided.
  \BackColour    = GetSysColor_(#COLOR_BTNFACE) ; Background
  \ScaleColour   = #Black                       ; Scale
  \KnobColour    = #Black                       ; Knob body
  \DotColour     = #Red                         ; Dot and line
  \CaptionColour = #Cyan                        ; Caption
  \FrameColourInner = #Green                    ; Frame 1
  \FrameColourOuter = #White                    ; Frame 2
  \CaptionFont   = "CourierNew"                 ; Caption font
  \CaptionFontHt = 12                           ; Caption font size
  \KnobTurns     = 1                            ; Rotations for for multi-turn
  \Caption       = "Test Knob"
  \WheelRateSlow = 1
  \WheelRateFast = 50
  
  ; Leave alone...
  \KnobMin       = 0
  \KnobMax       = 16383
  \KnobRate      = (\KnobMax - \KnobMin)/300
  
EndWith
;}
Procedure SetGadgetMouseXY(Win,Gadget,MX,MY)       ; Position mouse pointer at specified co-ordinated within a gadget
  MX + WindowX(Win,#PB_Window_InnerCoordinate) + GadgetX(Gadget)
  MY + WindowY(Win,#PB_Window_InnerCoordinate) + GadgetY(Gadget)
  SetCursorPos_(MX,MY)
EndProcedure

; Use these calls to adjust the defualt values for your next KnobGadget() --- BEFORE it is created.
; (You are ALSO free to adjust values in KnobDefaults\xxxx)
Procedure SetKnobBackgroundColour(C = $808080)
  KnobDefaults\BackColour = C
EndProcedure
Procedure SetKnobScaleColour(C = #Black)
  KnobDefaults\ScaleColour = C
EndProcedure
Procedure SetKnobColour(C = #Black)
  Shared KnobDefaults
  KnobDefaults\KnobColour = C
EndProcedure
Procedure SetKnobDotColour(C = #Red)
  KnobDefaults\DotColour = C 
EndProcedure
Procedure SetKnobCaptionColour(C = #Cyan)
  KnobDefaults\CaptionColour = C
EndProcedure
Procedure SetKnobCaptionFont(S.s,ch.i)
  KnobDefaults\CaptionFont   = S
  KnobDefaults\CaptionFontHt = ch
EndProcedure
Procedure SetKnobFrameColours(i.i,o.i)
  KnobDefaults\FrameColourInner  = i
  KnobDefaults\FrameColourOuter  = o
EndProcedure
Procedure SetKnobMultiTurn(i.i)
  KnobDefaults\KnobTurns = i
EndProcedure
Procedure SetKnobWheelRates(SloR.i,FstR.i)
  KnobDefaults\WheelRateSlow = SloR
  KnobDefaults\WheelRateFast = FstR
EndProcedure

; Modify knob parameters
Procedure SetKnobState(gadNum,Value.f=0) ;- Set KnobGadget() to value in range KnobMin to KnobMax
  Protected n,T,X,Y,Index.i = 0, Result.i = 0
  
  ; Locate caller's knob in KnobList
  n=1
  Index = 0
  ForEach KnobList()
    If KnobList() = gadNum
      Index = n    ; 1...
      Break
    EndIf
    n+1
  Next 
  
  ; Quit if not found
  If Index = 0
    ProcedureReturn #False
  EndIf
  
  
  With Knob(Index)
    
    ; Check for out-of-range values. Limit if over-range.
    If Value < \KnobMin : Value = \KnobMin : EndIf
    If Value > \KnobMax : Value = \KnobMax : EndIf 
    
    ; Save the caller's provided value for this knob
    If \ShiftFlag
      \PositionShift = Value 
    Else
      \Position = Value
    EndIf
    
    
    ; Send Windows callback message
    SendMessage_(WindowID(GetActiveWindow()),#PBM_KNOB,Index,GadgetID(gadNum))
    
    ; Re-draw the knob cap and pointer.
    
    ; Convert user value to first circle degrees 
    ; (This works for single and multi-turn pots.)
    Value - \KnobMin
    Value / \KnobRate                            ; 0..3x0 degrees
    While Value > 360 : Value - 360 : Wend       ; '%' cannot be used with FLOAT values
    
    ; Angle of pointer 
    If \flags & #MULTITURN1
      T = Value + 180
      T = 360 - T  
    Else
      T = -(Value + 30) 
    EndIf
    
    ; Calc co-ords of end of pointer
    X = \CentreX + (\KnobRad * Sin(Radian(T))) ; End of pointer X
    Y = \CentreY + (\KnobRad * Cos(Radian(T))) ; End of pointer Y  
   
    If StartDrawing(CanvasOutput(\CanvasNum))
        DrawImage(ImageID(\KnobImage),0,0)       ; Draw the knob backdrop
        ;{ Draw the knob and pointer feature
        If \flags & #JAZZ_top 
          ; Draw jazzy knob top and pointer
          DrawingMode(#PB_2DDrawing_Gradient)      
          BackColor(\KnobColour)
          FrontColor(\DotColour)
          ConicalGradient(\CentreX,\CentreY, T-90)     
          Circle(\CentreX,\CentreY,\KnobRad)
          Circle(X,Y,\KnobRad>>3,\DotColour)     ; Pointer dot
        Else  
          ; Draw standard knob top with line and pointer dot
          LineXY(\CentreX,\CentreY,X,Y,\DotColour)
          Circle(X,Y,\KnobRad>>3,\DotColour)
        EndIf
        ;}
        ;{ Draw dynamic caption on SWITCH... 
        ; (Unfinished: Will need FONT data)
        If \flags & #SWITCHKNOB
          If FindString(\Caption,"|")
            DrawingMode(#PB_2DDrawing_Transparent)
            k$ = StringField(\Caption,\Position-\KnobMin+1,"|")
            DrawText((\width-TextWidth(k$))/2,\width-8,k$,\CaptionColour)
          EndIf
        EndIf
        ;}
        ;{ Draw LED Indicator and set/clear KnobClick flag 
        If \flags & #LED_Switch
          DrawingMode(#PB_2DDrawing_Default)
          C = #Black
          \KnobClick = #False
          If (\Position - \KnobMin) > (\KnobMax-\KnobMin)/ 64
            C = #Red
            \KnobClick = #True
          EndIf
          Circle(14,14,4,C)
        EndIf
        ;}
        
        ; Keep pointer position etc for readout / next time
        If \ShiftFlag
          \LastShiftDotX = X 
          \LastShiftDotY = Y   
        Else
          \LastDotX = X 
          \LastDotY = Y    
        EndIf
        Result = #True
      StopDrawing()
       
    EndIf
  EndWith
   
  ProcedureReturn Result
EndProcedure
Procedure GetKnobState(gadNum)           ;- Get current position of a KnobGadget()
  Protected n,Result
  n=1
  ForEach KnobList()
    If KnobList() = gadNum
      Result = Knob(n)\Position
      Break
    EndIf
    n+1
  Next
  ProcedureReturn Result
EndProcedure
Procedure KnobSetRange(gadNum.i,KnobMin.i, KnobMax.i,KnobTurns.i=1)
  ; Modify the range and number of turns** of an existing KnobGadget
  ; ** MULTITURN1 knobs only.
  ; Search list for the KnobGadget() 
  n=1 : Index = 0
  ForEach KnobList()
    If KnobList() = gadNum
      Index = n         ; 1...
      Break
    EndIf
    n+1
  Next
  If Index = 0
    ProcedureReturn #False
  EndIf
  
  With Knob(Index)
    \KnobMin   = KnobMin 
    \KnobMax   = KnobMax
    \Position  = KnobMin
     
    If \flags & #MULTITURN1
      \KnobTurns = KnobTurns
      \KnobRate  = (\KnobMax - \KnobMin) / (360 * \KnobTurns) ; 'Clicks' per degree
    ElseIf \flags & #SWITCHKNOB
      \KnobTurns = 1
      \KnobRate  = (\KnobMax - \KnobMin) / 300 ; 'Clicks' per SWITCH STEP
    Else
      \KnobTurns = 1 
      \KnobRate   = (\KnobMax - \KnobMin) / 300
    EndIf
    
  EndWith
EndProcedure
Procedure KnobService()                  ;- Called when a KnobGadget() is active

  Protected n.i, Index.i, X.i, Y.i, F.f, dx.f, dy.f,T.i, dF.f,  Span.i, MyFont
  Static S.f,Dragging.i, WheelRate.i,LastGad.i = -1, MoveX.i = -1, MoveY.i = -1
  
  ; Which KnobGadget() called?
  Gad = EventGadget()
  
  ; Find gadget in table of knobs
  n=1
  Index = 0
  ForEach KnobList()
    If KnobList() = Gad
      Index = n         ; 1...
      Break
    EndIf
    n+1
  Next
  
  If Index = 0
    ProcedureReturn #False
  EndIf
  
  ; If knob has changed default the wheel rate to 1
  If Gad <> LastGad : WheelRate = 1 : EndIf
  LastGad = Gad
  
  With Knob(Index)
    
    Select EventType()
      Case  #PB_EventType_KeyDown
        ; If Dragging : Beep_(2000,55) : EndIf
          
        \ShiftFlag = GetGadgetAttribute(Gad,#PB_Canvas_Modifiers) & 1
        
      Case #PB_EventType_KeyUp
        ; If Dragging : Beep_(1000,55) : EndIf
        \ShiftFlag = GetGadgetAttribute(Gad,#PB_Canvas_Modifiers) & 1
        
      Case #PB_EventType_LeftButtonDown     ;{ User selects a Knob with left mouse button
        
        ; Move mouse pointer to last position used.        
        If \ShiftFlag
          SetKnobState(Gad,\PositionShift)
          SetGadgetMouseXY(GetActiveWindow(),\CanvasNum,\LastShiftDotX,\LastShiftDotY)
        Else
          SetKnobState(Gad,\Position)
          SetGadgetMouseXY(GetActiveWindow(),\CanvasNum,\LastDotX,\LastDotY)
        EndIf
        
        Dragging = Index
        ;}
      Case #PB_EventType_LeftButtonUp       ;{ Finish when user releases left mouse button
        Dragging = 0
        ;}
      Case #PB_EventType_MouseWheel         ;{ Move pointer via mouse wheel   
        T = GetGadgetAttribute(Gad,#PB_Canvas_WheelDelta)            ; Get mouse delta
        SetKnobState(Gad,GetKnobState(Gad)+(T * WheelRate))          ; Apply it to Knob
        ;}
      Case #PB_EventType_MiddleButtonDown   ;{ Adjust wheel rate...
        WheelRate = \WheelRateFast
      Case #PB_EventType_MiddleButtonUp 
        WheelRate = \WheelRateSlow
        ;}
    EndSelect
    
    If Dragging                             ;{ Moving Potentiometer and Switch knobs
      
      ; Get mouse position over Canvas()
      X = GetGadgetAttribute(Gad,#PB_Canvas_MouseX)
      Y = GetGadgetAttribute(Gad,#PB_Canvas_MouseY)
      
      ; If mouse has moved from previous position...
      If (X <> MoveX) Or (Y <> MoveY)
        MoveX = X : MoveY = Y
        
        If X 
          
          ; Adjust X and Y for framed version of knob
          If (\flags & #BORDER)
            Y + 2 : X + 2 
          EndIf
          
          ; Calculate angle of mouse relative to knob centre in Degrees
          dx = X - \CentreX 
          dy = Y - \CentreY
          F  = Degree(ATan2(dx,dy))
          
          ; Experiment : Make mouse track knob periphery... (Not finished.)
          ; SetGadgetMouseXY(GetActiveWindow(),Gad, \CentreX + (\KnobRad * Cos(Radian(F))), \CentreY + (\KnobRad * Sin(Radian(F))) )
          
          If  \flags & (#POTKNOB | #SWITCHKNOB);{
            If F < 0 : F = 360 + F : EndIf
            F + 240                           ; Offset because pot's '0' is not at cardinal point
            If F>360 : F-360 : EndIf          ; Circular wrap around?
            If F>302 : F=0   : EndIf          ; Potentiometers have 300 degree travel.
            
            ; Convert degrees of rotation to User value
            F * \KnobRate
            F + \KnobMin
            
            ; Limit the range
            If F < \KnobMin : F = \KnobMin : EndIf
            If F > \KnobMax : F = \KnobMax : EndIf
            
            ; Limit steps size and also forces dead-band at bottom. Neat :-)
            If \ShiftFlag
              If Abs(F - \PositionShift) < (60 * \KnobRate)
                SetKnobState(Gad, Round (F,#PB_Round_Nearest))
              EndIf
            Else
              If Abs(F - \Position) < (60 * \KnobRate)
                SetKnobState(Gad, Round (F,#PB_Round_Nearest))
              EndIf
            EndIf
            
            ;}
          ElseIf \flags & #MULTITURN1              ;{ MULTITURN1 - Knob tracks mouse and value increase by 1/\Turns
            ; (There must be a neater way!!!)
            
            F + 90                                                          ; Move '0' to top.
            If F < 0 : F = 360 + F : EndIf                                  ; Roll-over correction
            
            ; Calculate knob revolution... so far
            Span = (\KnobMax  - \KnobMin) / \KnobTurns                      ; Steps per revolution
            If \ShiftFlag
              Turn = Int((\PositionShift - \KnobMin)/Span)                  ; Completed turns of knob 
            Else
              Turn = Int((\Position - \KnobMin)/Span)
            EndIf
            
            S = F - \LastF                                                  ; Movement since last time
            \LastF = F
            
            ; Detect mouse passing through 'Top Dead Centre' (=TDC) and adjust 'Turns'
            If Abs(S) > 320                                                 ; 0=>360 or 360=>0
              ; Manage TDC transitions for first, last and in-between turns.
              ; (\Gate: 0 = free to move up or down, -1 = Jammed at min, +1 = Jammed at max)
              
              Select \Gate
                Case 1                                                       ; At top, can only decrease...
                  If Sign(S) <> -1
                    Turn  = \KnobTurns - 1
                    F     = 359
                    \Gate = 0
                  EndIf
                Case -1                                                      ; At bottom, can only increase...
                  If Sign(S) <> 1
                    Turn  = 0
                    F     = 0
                    \Gate = 0
                  EndIf
                Case 0                                                       ; Can move up or down...
                  If Sign(S) =  1 And Turn = 0            :\Gate = -1: EndIf ; Reached min?
                  If Sign(S) = -1 And Turn = \KnobTurns-1 :\Gate =  1: EndIf ; Reached max?
                  If \Gate   =  0 : Turn - Sign(S) : EndIf                   ; Free to move...
              EndSelect
            EndIf 
            
            ; Convert knob angle to control clicks and update display
            If \Gate = 0                                                     ; Free to move, so
              F * \KnobRate                                                  ; convert knob angle degrees to 'Clicks'
              F + (Turn * Span)                                              ; add 'Clicks' for completed turns, 
              F + \KnobMin                                                   ; add control's minimum value and
              SetKnobState(Gad,Round(F,#PB_Round_Nearest))                   ; move the control.
            EndIf
            ;}
          EndIf
          
        EndIf
      EndIf
      ;}
    EndIf
    
    
  EndWith
EndProcedure
Procedure KnobGadget(gadNum.i, X.i, Y.i, Size.i,Caption.s = "", KnobMin.i= 0, KnobMax.i = 10000, flags.i = #POTKNOB) ; Create a new Knob()
  ; Create a Circular Control Knob looking like a traditional radio receiver or hi-fi control.
  
  ; gadNum  = Number of gadget to be created, else #PB_Any and this procedure returns number assigned by PB.
  ; X, Y    = Gadget co-ordinates
  ; Size    = Width of knob and backdrop. 
  ;           Height = Width + height of caption (if specified)
  ; Caption = Name of control. For SWITCHKNOB specify one name for each position, with '|' separators.
  ; KnobMin = Start of range of values returned by control.
  ; KnobMax = End of range of values returned by control. 
  ; Flags   = Specify the knob type and various options.
  
  ; Knob types... call KnobGadget() with Flags including just ONE of these.
  ; #POTKNOB       An emulation of a standard potentiometer (eg: Radio volume control) DEFAULT
  ; #MULTITURN1    A multi turn knob to provide finer granularity.  ** 
  ; #MULTITURN2    For future use                                   ** 
  ; #SWITCHKNOB    An emulation of a rotary switch with four or more positions.
  ; #PLAINKKNOB    A very simple knob with no space for embelishments
  
  ; Additional flags to control optional features...
  ; #RING_Heat     Put gradient coloured ring around knob, black through to yellow via red.
  ; #FRAME_Switch  Place standard PB/Windows frame around KnobGadget
  ; #JAZZ_top      Replace Line+Dot on top of knob with colourful position indicator
  ; #BORDER        Place a coloured border around the knob. See SetKnobFrameColours()
  ; #LED_Switch    Provide a Black/Red 'LED', red when value is greater than range/64 

  ; USAGE
  ; Read position of a knob with GetKnobState(KnobNum) or with with WinCallback()
  ; Set position of a knob with SetKnobState(KnobNum,Value)
  
  Protected Result.i, w.i, h.i, dx.f, dy.f, cx.f, cy.f
  Static KnobCount = 1
  
  ; Set the 'h' and 'w' from caller's values, adjust height for caption.
  Size | 1 ; Size must be odd, so there is a pixel at the centre
  w = Size :  h = Size 
  If Caption
    h + KnobDefaults\CaptionFontHt + 2
  EndIf
 
  ; Create flags and the CanvasGadget()
  T = #PB_Canvas_ClipMouse|#PB_Canvas_Keyboard|#PB_Canvas_DrawFocus
  If (flags & #BORDER) 
    T | #PB_Canvas_Border
  EndIf
  Result = CanvasGadget(gadNum, X, Y, w, h,T)
  
  If Result <> 0
    
    ; Assign gadget number
    If gadNum = #PB_Any : gadNum = Result : EndIf
    
    ; Extend storage for another set of knob definitions
    If KnobCount > 1 : Redim Knob(KnobCount) : EndIf
    
    With Knob(KnobCount)     
    ;{ Complete the knob definitions
      \CanvasNum = gadNum
      \width     = w 
      \height    = h 
      If (flags & #BORDER)
        \width - 4  : \height - 4
      EndIf   
      
      \KnobImage = CreateImage(#PB_Any,\width*#Scale,\height*#Scale)
      \CentreX  = \width >> 1
      \CentreY  = \width >> 1  
      
      \KnobRad  = \width >> 2
      If flags & #PLAINKKNOB
        \KnobRad  = \width>>1 - \width>>3
      EndIf
       
      \Caption  = Caption
      \flags    = flags
      
      \KnobMin  = KnobMin 
      \KnobMax  = KnobMax
      \Position = KnobMin
       
      
      If \flags & #MULTITURN1
        \KnobTurns = KnobDefaults\KnobTurns
        \KnobRate  = (\KnobMax - \KnobMin) / (360 * \KnobTurns) ; 'Clicks' per degree
      ElseIf \flags & #SWITCHKNOB
        \KnobTurns = 1
        \KnobRate  = (\KnobMax - \KnobMin) / 300 ; 'Clicks' per SWITCH STEP
      Else
        \KnobTurns = 1 ; Pots and switches always 1
        \KnobRate   = (\KnobMax - \KnobMin) / 300
      EndIf
      
      \KnobColour       = KnobDefaults\KnobColour
      \BackColour       = KnobDefaults\BackColour
      \ScaleColour      = KnobDefaults\ScaleColour
      \CaptionColour    = KnobDefaults\CaptionColour
      \CaptionFont      = KnobDefaults\CaptionFont
      \CaptionFontHt    = KnobDefaults\CaptionFontHt
      \DotColour        = KnobDefaults\DotColour
      \FrameColourInner = KnobDefaults\FrameColourInner
      \FrameColourOuter = KnobDefaults\FrameColourOuter
      \WheelRateSlow    = KnobDefaults\WheelRateSlow
      \WheelRateFast    = KnobDefaults\WheelRateFast
      
      ;} 
      ; Create SCALED UP version of image of knob's fixed features
      MyFont = LoadFont(#PB_Any,\CaptionFont,\CaptionFontHt * #Scale)
      StartDrawing(ImageOutput(\KnobImage))
        DrawingFont(FontID(MyFont))
        ;{ Backdrop
        Box(0,0,\width*#Scale,\height*#Scale,\BackColour)
        ;}
        ;{ Optional coloured border with rounded corners
        If flags & #FRAME_Switch
          n = 1 : R = 12
          RoundBox(n*#Scale, n*#Scale,(\width-(2*n))*#Scale,(\height-(2*n))*#Scale,R*#Scale,R*#Scale,\FrameColourInner)
          n = 4 : R = 09
          RoundBox(n*#Scale, n*#Scale,(\width-(2*n))*#Scale,(\height-(2*n))*#Scale,R*#Scale,R*#Scale,\FrameColourOuter)
          n = 7 : R = 06
          RoundBox(n*#Scale, n*#Scale,(\width-(2*n))*#Scale,(\height-(2*n))*#Scale,R*#Scale,R*#Scale,\BackColour)
        EndIf
        ;}
        ;{ Radial markers 
        If Not(flags & #PLAINKKNOB)
          ; Calculate start angle, end and step size
          R = (\width>>1 - \width>>4 ) * #Scale ; Radius of marker lines
          A = 30                                ; Start angle (degrees)
          B = 330                               ; End angle
          S = 30                                ; Step size
          
          If flags & #MULTITURN1 : B = 360 : EndIf
          
          If flags & #SWITCHKNOB
            A = 30
            B = 330
            S = (B - A) / (\KnobMax - \KnobMin)
          EndIf
          
          ; Draw the markers
          T = A
          Repeat
            X     = \CentreX*#Scale + (R*Sin(Radian(T))) ; End of pointer X
            Y     = \CentreY*#Scale + (R*Cos(Radian(T))) ; End of pointer Y
            cx    = \CentreX*#Scale                     
            cy    = \CentreY*#Scale                     
            
            ; Draw several adjacent lines to create a single thick one.
            For n = -5 To 5
              dx = n * Cos(Radian(T)) : dy = n * Sin(Radian(T))
              LineXY(cx+dx, cy-dy, X+dx, Y-dy, \ScaleColour)
            Next
            
            T + S
          Until T > B
        EndIf
        ;}
        ;{ Optional coloured ring around the knob
        If flags & #RING_Heat
          DrawingMode(#PB_2DDrawing_Gradient)  
          BackColor( $000000)
          GradientColor(0.2,#Black)
          GradientColor(0.5, #Yellow);$00FFFF)
          FrontColor($0000FF)
          
          ConicalGradient(\CentreX*#Scale,\CentreY*#Scale, 300)     
          Circle(\CentreX*#Scale,\CentreY*#Scale,(\KnobRad+\width>>4-1)*#Scale)
          DrawingMode(#PB_2DDrawing_Default )
        Else
          ; Normal - just clear a space 
          Circle(\CentreX*#Scale,\CentreY*#Scale,(\KnobRad+\width>>4-1)*#Scale,\BackColour)
        EndIf
        ;}
        ;{ Optional 'LED'
        If flags & #LED_Switch
          T = 14 * #Scale
          Circle(T,T,6*#Scale,#Black)
        EndIf
        ;}
        ;{ Draw the knob top
        Circle(\CentreX*#Scale,\CentreY*#Scale,\KnobRad*#Scale,\KnobColour)
        ;}
        ;{ Draw the caption
        DrawingFont(FontID(MyFont))
        BackColor(\BackColour)
        DrawingMode(#PB_2DDrawing_Default)
        If Not(\flags & #SWITCHKNOB And CountString(\Caption,"|"))
          X = (#Scale*\width - TextWidth(\Caption))/2
          DrawText(X,#Scale*(\width-12),\Caption,\CaptionColour)
        EndIf
        ;}
      StopDrawing()
      FreeFont(MyFont)
      
      ; Reduce to wanted size and draw on CanvasGadget()
      ResizeImage(\KnobImage,\width,\height)
      StartDrawing(CanvasOutput(\CanvasNum))
        DrawImage(ImageID(\KnobImage),0,0)
      StopDrawing() 
      
      SetGadgetAttribute(\CanvasNum,#PB_Canvas_Cursor,#PB_Cursor_Hand)

    
    ; Add the new knob gadget number to the list
    AddElement(KnobList())
    KnobList() = gadNum
    SetKnobState(gadNum,0)
    
    \LastShiftDotX = \LastDotX
    \LastShiftDotY = \LastDotY
    \PositionShift = \Position
    
     EndWith
    BindGadgetEvent(gadNum,@KnobService())
    
    KnobCount + 1 ; Ready for next Knob!
    
  EndIf
  
  ProcedureReturn gadNum
EndProcedure


CompilerIf #PB_Compiler_IsMainFile
  ; *************************************
  ;            Test code
  ;{ *************************************
  
  Enumeration 1000 ; Define Window, gadget numbers etc...
    #Win_KMain
    
    #Gad_Knob1    ; It is helpful to have the knobes grouped
    #Gad_Knob2    ; together
    #Gad_Knob3
    
    #Gad_Knob4
    #Gad_Knob5
    #Gad_Knob6
    #Gad_Knob7
    #Gad_Knob8
    #Gad_Knob9
    #Gad_Knob10
    #Gad_Knob11
    #Gad_Knob12
    #Gad_Knob13
    #Gad_Knob14
    
    #Gad_Text1
    #Gad_Text2
    #Gad_Text3
    #Gad_Text4
    #Gad_Text5
    
    ; More here
  EndEnumeration
  Procedure WinCallback(hwnd, uMsg, wParam, lParam) ; Standard PB Windows callback...
    
    
    Select uMsg  
     
      Case #PBM_KNOB   ; Receiving Knob() messages in 'real time'
        Select lParam
          Case GadgetID(#Gad_Knob1) : SetGadgetText(#Gad_Text1, "Knob1  = "+Str(Knob(wParam)\Position))
          Case GadgetID(#Gad_Knob2) : SetGadgetText(#Gad_Text2, "Knob2  = "+Str(Knob(wParam)\Position))
          Case GadgetID(#Gad_Knob3) : SetGadgetText(#Gad_Text3, "Knob3  = "+Str(Knob(wParam)\Position))
          Case GadgetID(#Gad_Knob4) : SetKnobState(#Gad_Knob8,  GetKnobState(#Gad_Knob4)) ; Slave two knobs... one way, 4=>8
          Case GadgetID(#Gad_Knob12): SetGadgetText(#Gad_Text4, "Knob12 = "+Str(Knob(wParam)\Position))
          Case GadgetID(#Gad_Knob14): SetGadgetText(#Gad_Text5, "Sw  14 = "+Str(Knob(wParam)\Position)) 
          
          ; Good place to check Get/SetKnobState()  
        EndSelect
        
        ; etc
        ; etc
        
    EndSelect
    
    ProcedureReturn #PB_ProcessPureBasicEvents 
    
  EndProcedure
  
  OpenWindow(#Win_KMain,0,0,900,450,"Knob test Rev 1.02",#PB_Window_ScreenCentered|#PB_Window_SystemMenu)
  
  ; User defined default values
  SetKnobScaleColour(#White)
  SetKnobColour(#Black)
  SetKnobDotColour(#Red)
  SetKnobCaptionColour(#White)
  SetKnobCaptionFont("CourierNew",18)

  ; *************** Create Knobs ****************
  ; #RING_Heat    
  ; #FRAME_Switch
  ; #JAZZ_top
  ; #BORDER
  ; #LED_Switch
  
  ; Change knob default values BEFORE creating a Knob
  SetKnobDotColour(#Red)
  KnobGadget(#Gad_Knob1,40,20,150,"TREBLE",0,1000,#POTKNOB | #RING_Heat | #FRAME_Switch | #LED_Switch) ; Frame and HeatRing
  SetKnobState(#Gad_Knob1,50)
  
  SetKnobScaleColour(#Black)
  SetKnobDotColour(#White)
  SetKnobCaptionColour(#Blue)
  KnobGadget(#Gad_Knob2,195,20,150,"BASS",0,10000,#POTKNOB | #BORDER | #JAZZ_top)
  
  SetKnobScaleColour(#Yellow)
  SetKnobDotColour(#Blue)
  SetKnobColour(#Cyan)
  SetKnobCaptionColour(#Red)
  KnobGadget(#Gad_Knob3,350,20,150,"VOLUME",1,42,#POTKNOB |#RING_Heat |#JAZZ_top | #FRAME_Switch) 
  
  SetKnobScaleColour(#Black)
  SetKnobDotColour(#White)
  SetKnobColour(#Black)
  SetKnobMultiTurn(5)
  KnobGadget(#Gad_Knob12,510,20,200,"MultiTurn (5T)",0,3000,#MULTITURN1 |#JAZZ_top | #FRAME_Switch | #LED_Switch)
  SetKnobState(#Gad_Knob12,1000)
  
  SetKnobDotColour(#Yellow)
  SetKnobCaptionColour(#Blue)
  SetKnobWheelRates(1,1)
  KnobGadget(#Gad_Knob14,720,20,150,"160M|80M|40M|20M|15M|10M|6M|x7|x8|x9|",10,19,#SWITCHKNOB | #JAZZ_top)
 
  
  SetKnobDotColour(#White)
  SetKnobCaptionFont("CourierNew",12)
  SetKnobBackgroundColour($FFAA55)
  SetKnobWheelRates(1,50)
  
  SetKnobCaptionColour(#White)
  KnobGadget(#Gad_Knob4, 10 ,280,100,"Channel 1",0,10000,#POTKNOB | 8<<5)
  KnobGadget(#Gad_Knob5, 110,280,100,"Channel 2",0,10000,#POTKNOB | 9<<5) 
  KnobGadget(#Gad_Knob6, 220,280,100,"Channel 3",0,10000,#POTKNOB | 2<<5) 
  KnobGadget(#Gad_Knob7, 320,280,100,"Channel 4",0,10000,#POTKNOB | 3<<5) 
  
  SetKnobScaleColour(#Black)
  KnobGadget(#Gad_Knob8, 430,280,100,"Channel 5",0,10000,#POTKNOB | 4<<5) 
  KnobGadget(#Gad_Knob9, 530,280,100,"Channel 6",0,10000,#POTKNOB | 5<<5)
  KnobGadget(#Gad_Knob10,640,280,100,"Channel 7",0,10000,#POTKNOB | 6<<5) 
  KnobGadget(#Gad_Knob11,740,280,100,"Channel 8",0,10000,#POTKNOB | 7<<5)
  
  DisableGadget(#Gad_Knob8,#True)
  ; ******************************************** 
  
  ; To show knob values returned from Window callback
  TextGadget(#Gad_Text1,40, 200,150,20,"",#PB_Text_Center)
  TextGadget(#Gad_Text2,190,200,150,20,"",#PB_Text_Center)
  TextGadget(#Gad_Text3,340,200,160,20,"",#PB_Text_Center)
  TextGadget(#Gad_Text4,510,250,200,20,"",#PB_Text_Center)
  TextGadget(#Gad_Text5,720,195,150,15,"",#PB_Text_Center)
  
  ; Optional: Callabck to receive 'real-time' knob messages
  SetWindowCallback(@WinCallback()) 
  
  
  KnobSetRange(#Gad_Knob1,42,420,1)  : SetGadgetState(#Gad_Knob1,42)
  
  ; Dispatch
  WheelRate.i = 1
  Repeat
    Select WaitWindowEvent()
      Case #PB_Event_CloseWindow
        Break
        
      Case #PB_Event_Gadget
        Select EventGadget()
            
            ; All usual stuff here....
            
        EndSelect
        
  EndSelect

  ForEver
  ;}
 CompilerEndIf

Re: KnobGadget() - Update March 2016 Rev 2.0

Posted: Fri Mar 11, 2016 6:56 pm
by RSBasic
Image

Re: KnobGadget() - Update March 2016 Rev 2.0

Posted: Sat Mar 19, 2016 2:34 am
by Psychophanta
I like it. Useful.
Thanks Richards!