Page 1 of 1

Skinnable Window (buttons, draggable, resizable)

Posted: Thu Feb 09, 2017 2:38 am
by eddy
Based on post : http://www.purebasic.fr/english/viewtop ... 13&t=66672
Coded in PB5.60+

Code: Select all


DeclareModule SkinnableWindow
   Global NewMap SkinColors.i()
   Global NewMap SkinSizes.i()
   
   Declare InitSkin()
   Declare SkinWindow(Window, IsResizable=#True)
EndDeclareModule

Module SkinnableWindow
   EnableExplicit
   Prototype DrawCustomSkin(*skinData)
   Structure SKIN_ITEM
      name$
      parentBorder.i
      hit.q
      ;relative position for drawing
      top.f
      left.f
      right.f
      bottom.f
      ;absolute position for hit detection
      absTop.f
      absLeft.f
      absRight.f
      absBottom.f
   EndStructure
   Structure SKIN_DATA
      window.i
      isResizable.b
      isActive.b
      ;border gadgets
      activeBorder.i
      upBorder.i
      downBorder.i
      leftBorder.i
      rightBorder.i
      ;hit detection
      currentHit.q
      previousHit.q
      ;capture handler
      captureHit.q
      captX.i
      captY.i
      ;drawing
      drawCustomBorders.DrawCustomSkin
      drawCustomBorderItems.DrawCustomSkin
      List items.SKIN_ITEM()
   EndStructure   
   Global NewList SkinItems.SKIN_ITEM()
   Global NewMap SkinData.SKIN_DATA()
   
   EnumerationBinary 
      #UP_BORDER
      #DOWN_BORDER
      #LEFT_BORDER
      #RIGHT_BORDER
      #CAPTION_BAR
      #CAPTION_TITLE
      #CAPTION_ICON
      #BUTTON_CLOSE
      #BUTTON_MIN
      #BUTTON_MAX
      ;combined flags for checking hit
      #ANY_BORDERS=#UP_BORDER | #DOWN_BORDER | #LEFT_BORDER | #RIGHT_BORDER
      #ANY_CAPTION=#CAPTION_BAR | #CAPTION_ICON | #CAPTION_TITLE
   EndEnumeration
   Procedure AddBorderItem(border$, name$, top.f, left.f, right.f, bottom.f, customHit=0)
      ResetList(SkinItems())
      Protected *skinZone.SKIN_ITEM=InsertElement(SkinItems())
      With *skinZone
         \name$=name$
         Select LCase(name$)
            Case "caption"
               \hit=#CAPTION_BAR
            Case "title"
               \hit=#CAPTION_TITLE
            Case "icon"
               \hit=#CAPTION_ICON
            Case "close"
               \hit=#BUTTON_CLOSE
            Case "max"
               \hit=#BUTTON_MAX
            Case "min"
               \hit=#BUTTON_MIN
            Default
               \hit=customHit
         EndSelect
         Select LCase(border$)
            Case "up"
               \parentBorder=#UP_BORDER
            Case "down"
               \parentBorder=#DOWN_BORDER
            Case "left"
               \parentBorder=#LEFT_BORDER
            Case "right"
               \parentBorder=#RIGHT_BORDER
            Default
               DebuggerError("border$ accepts one of following values: up, down, left, right")
         EndSelect
         \top=top
         \left=left
         \right=right
         \bottom=bottom
      EndWith
   EndProcedure
   
   Procedure DrawBorderItems(*skinData.SKIN_DATA)      
      With *skinData
         Protected x, y, w, h, cBox, cIcon 
         Protected BorderSize=SkinSizes("border"), CornerSize=SkinSizes("corner"), CaptionSize=SkinSizes("caption")
         
         StartVectorDrawing(CanvasVectorOutput(\upBorder))         
         ;close
         If \currentHit & #BUTTON_CLOSE
            cBox=SkinColors("button_close")
            cIcon=SkinColors("stroke_hover")
         ElseIf \isActive
            cBox=SkinColors("button")
            cIcon=SkinColors("stroke")
         Else
            cBox=SkinColors("button_inactive")
            cIcon=SkinColors("stroke_inactive")
         EndIf
         w=45 : h=CaptionSize : x=GadgetWidth(\upBorder)-BorderSize-w : y=0
         ResetCoordinates()
         TranslateCoordinates(x, y)
         AddPathBox(0, 0, w, h)         
         VectorSourceColor(cBox)
         FillPath()
         VectorSourceColor(cIcon)
         MovePathCursor(w / 2-5, h / 2 - 5)
         AddPathLine(w / 2 + 4, h / 2 + 4)
         StrokePath(1)
         MovePathCursor(w / 2-5, h / 2 + 4)
         AddPathLine(w / 2 + 4, h / 2-5)
         StrokePath(1)
         ;maximize
         If \currentHit & #BUTTON_MAX
            cBox=SkinColors("button_hover")
            cIcon=SkinColors("stroke_hover")
         ElseIf \isActive
            cBox=SkinColors("button")
            cIcon=SkinColors("stroke")
         Else
            cBox=SkinColors("button_inactive")
            cIcon=SkinColors("stroke_inactive")
         EndIf
         ResetCoordinates()
         TranslateCoordinates(x-w, y)
         AddPathBox(0, 0, w, h)         
         VectorSourceColor(cBox)
         FillPath()
         VectorSourceColor(cIcon)
         AddPathBox(w / 2-5, h / 2-5, 10, 10)
         StrokePath(1)
         ;minimize
         If \currentHit & #BUTTON_MIN
            cBox=SkinColors("button_hover")
            cIcon=SkinColors("stroke_hover")
         ElseIf \isActive
            cBox=SkinColors("button")
            cIcon=SkinColors("stroke")
         Else
            cBox=SkinColors("button_inactive")
            cIcon=SkinColors("stroke_inactive")
         EndIf
         ResetCoordinates()
         TranslateCoordinates(x-w*2, y)
         AddPathBox(0, 0, w, h)
         VectorSourceColor(cBox)
         FillPath()
         VectorSourceColor(cIcon)
         MovePathCursor(w / 2-5, h / 2)
         AddPathLine(w / 2 + 4, h / 2)
         StrokePath(1)
         StopVectorDrawing()
      EndWith
   EndProcedure
   
   Procedure DrawBorders(*skinData.SKIN_DATA)
      With *skinData
         If \DrawCustomBorders
            \DrawCustomBorders(*skinData)
         Else
            ; Draw 4 borders
            If \isActive : Protected BorderColor=SkinColors("border") : Else : BorderColor=SkinColors("border_inactive") : EndIf
            StartDrawing(CanvasOutput(\upBorder))
            Box(0, 0, OutputWidth(), OutputHeight(), BorderColor)
            StopDrawing()            
            StartDrawing(CanvasOutput(\leftBorder))
            Box(0, 0, OutputWidth(), OutputHeight(), BorderColor)
            StopDrawing()
            StartDrawing(CanvasOutput(\downBorder))
            Box(0, 0, OutputWidth(), OutputHeight(), BorderColor)
            StopDrawing()
            StartDrawing(CanvasOutput(\rightBorder))
            Box(0, 0, OutputWidth(), OutputHeight(), BorderColor)
            StopDrawing()
         EndIf
         If \DrawCustomBorderItems
            \DrawCustomBorderItems(*skinData)
         Else
            ; Draw border items
            DrawBorderItems(*skinData.SKIN_DATA)
         EndIf         
      EndWith      
   EndProcedure
   
   Procedure DrawShadow(*skinData.SKIN_DATA, IsEnabled=#True, TransparencyColor=#Cyan)
      CompilerIf #PB_Compiler_OS=#PB_OS_Windows
         Static *dwmapi
         Static *DwmExtendFrameIntoClientArea
         Static *DwmSetWindowAttribute
         Protected hWnd=WindowID(*skinData\window)
         
         If Not *dwmapi
            *dwmapi=GetModuleHandle_("dwmapi")
            If *dwmapi And Not *DwmExtendFrameIntoClientArea And Not *DwmSetWindowAttribute
               *DwmExtendFrameIntoClientArea=GetProcAddress_(*dwmapi, Ascii("DwmExtendFrameIntoClientArea"))
               *DwmSetWindowAttribute=GetProcAddress_(*dwmapi, Ascii("DwmSetWindowAttribute"))
            EndIf
         EndIf
         
         If *DwmExtendFrameIntoClientArea And *DwmSetWindowAttribute
            CompilerIf Not Defined(DWMWA_NCRENDERING_POLICY, #PB_Constant) : #DWMWA_NCRENDERING_POLICY=2 : CompilerEndIf
            CompilerIf Not Defined(DWMNCRP_ENABLED, #PB_Constant) : #DWMNCRP_ENABLED=2 : CompilerEndIf
            
            If IsEnabled
               ;transform into opaque layered window
               SetWindowLongPtr_(hWnd, #GWL_EXSTYLE, GetWindowLongPtr_(hWnd, #GWL_EXSTYLE) | #WS_EX_LAYERED)
               SetLayeredWindowAttributes_(hWnd, TransparencyColor, 255, #LWA_COLORKEY)
            Else
               SetWindowLongPtr_(hWnd, #GWL_EXSTYLE, GetWindowLongPtr_(hWnd, #GWL_EXSTYLE) & ~#WS_EX_LAYERED)
            EndIf
            
            ;enable drop shadow
            Protected *Margins=AllocateMemory(4*SizeOf(Long))
            FillMemory(*Margins, 4*SizeOf(Long), IsEnabled, #PB_Long)
            Protected value.Integer\i=#DWMNCRP_ENABLED
            CallFunctionFast(*DwmSetWindowAttribute, hWnd, #DWMWA_NCRENDERING_POLICY, value, SizeOf(value))
            CallFunctionFast(*DwmExtendFrameIntoClientArea, hWnd, *Margins)
         EndIf
         ProcedureReturn -1
      CompilerEndIf
   EndProcedure
   
   Procedure ResizeBorderItems(*skinData.SKIN_DATA)
      With *skinData
         Protected win=\window, parentBorder
         ClearList(\items())
         ForEach SkinItems()
            Select SkinItems()\parentBorder
               Case #UP_BORDER : parentBorder=\upBorder
               Case #DOWN_BORDER : parentBorder=\downBorder
               Case #LEFT_BORDER : parentBorder=\leftBorder
               Case #RIGHT_BORDER : parentBorder=\rightBorder
            EndSelect
         Protected top=SkinItems()\top + Bool(Sign(SkinItems()\top)<0) * GadgetHeight(parentBorder)
         Protected left=SkinItems()\left + Bool(Sign(SkinItems()\left)<0) *GadgetWidth(parentBorder)
         Protected right=SkinItems()\right + Bool(Sign(SkinItems()\right)<0) *GadgetWidth(parentBorder)
         Protected bottom=SkinItems()\bottom + Bool(Sign(SkinItems()\bottom)<0) * GadgetHeight(parentBorder) 
            AddElement(\items())
            \items()=SkinItems()
            \items()\parentBorder=parentBorder
            \items()\top=top
            \items()\left=left
            \items()\right=right
            \items()\bottom=bottom
            \items()\absTop=top + GadgetY(parentBorder)
            \items()\absLeft=left + GadgetX(parentBorder)
            \items()\absRight=right + GadgetX(parentBorder)
            \items()\absBottom=bottom + GadgetY(parentBorder)
         Next
      EndWith
   EndProcedure
   
   Procedure ResizeBorders(*skinData.SKIN_DATA)
      With *skinData
         Protected win=\window
         Protected BorderSize=SkinSizes("border"), CornerSize=SkinSizes("corner"), CaptionSize=SkinSizes("caption")
         ;resize border gadgets
         ResizeGadget(\upBorder, 0, 0, WindowWidth(win), CaptionSize)
         ResizeGadget(\leftBorder, 0, CaptionSize, BorderSize, WindowHeight(win) - BorderSize - CaptionSize)
         ResizeGadget(\rightBorder, WindowWidth(win) - BorderSize, CaptionSize, BorderSize, WindowHeight(win) - BorderSize - CaptionSize)
         ResizeGadget(\downBorder, 0, WindowHeight(win) - BorderSize, WindowWidth(win), BorderSize)
         ;calculate position of border items
         ResizeBorderItems(*skinData.SKIN_DATA)
      EndWith      
   EndProcedure
   
   Procedure.i HitBorders(*skinData.SKIN_DATA)
      With *skinData
         Protected win=\window
         Protected gadget=\activeBorder
         Protected mouseX=WindowMouseX(win)
         Protected mouseY=WindowMouseY(win)
         Protected BorderSize=SkinSizes("border"), CornerSize=SkinSizes("corner"), CaptionSize=SkinSizes("caption")
         \currentHit=0
         ; Detect borders
         If \isResizable
            If (mouseX>-1 And mouseX<CornerSize)
               \currentHit | #LEFT_BORDER
            EndIf
            If (mouseY>-1 And mouseY<CornerSize)
               \currentHit | #UP_BORDER
            EndIf
            If (mouseX>=WindowWidth(win) - CornerSize)
               \currentHit | #RIGHT_BORDER
            EndIf
            If (mouseY>=WindowHeight(win) - CornerSize)
               \currentHit | #DOWN_BORDER
            EndIf
         EndIf         
         ; Change cursor
         Select \currentHit
            Case 0
               SetGadgetAttribute(gadget, #PB_Canvas_Cursor, #PB_Cursor_Default)
            Case #UP_BORDER, #DOWN_BORDER
               SetGadgetAttribute(gadget, #PB_Canvas_Cursor, #PB_Cursor_UpDown)
            Case #LEFT_BORDER, #RIGHT_BORDER
               SetGadgetAttribute(gadget, #PB_Canvas_Cursor, #PB_Cursor_LeftRight)
               CompilerIf #PB_Compiler_OS=#PB_OS_Windows
               Case #LEFT_BORDER | #UP_BORDER, #RIGHT_BORDER | #DOWN_BORDER
                  SetGadgetAttribute(gadget, #PB_Canvas_Cursor, #PB_Cursor_LeftUpRightDown)
               Case #LEFT_BORDER | #DOWN_BORDER, #RIGHT_BORDER | #UP_BORDER
                  SetGadgetAttribute(gadget, #PB_Canvas_Cursor, #PB_Cursor_LeftDownRightUp)
               CompilerElse
               Default
                  SetGadgetAttribute(gadget, #PB_Canvas_Cursor, #PB_Cursor_Cross)
               CompilerEndIf
         EndSelect
         ; Detect zones
         ForEach \items()
            If mouseX>=\items()\absLeft And mouseX<=\items()\absRight And mouseY>=\items()\absTop And mouseY<=\items()\absBottom
               \currentHit | \items()\hit
               Break
            EndIf
         Next
         ProcedureReturn \currentHit
      EndWith
   EndProcedure
   
   Procedure SkinEvents()
      Static rect.RECT
      Protected event=Event()
      Protected eventType=EventType()
      Protected gadget=EventGadget()
      Protected win=EventWindow()
      Protected *skinData.SKIN_DATA=SkinData("" + win)
      With *skinData      
         If event=#PB_Event_ActivateWindow
            \isActive=#True
            DrawBorders(*skinData)
         ElseIf event=#PB_Event_DeactivateWindow
            \isActive=#False
            DrawBorders(*skinData)
         ElseIf event=#PB_Event_Gadget 
            \activeBorder=gadget
            Select eventType
               Case #PB_EventType_MouseEnter, #PB_EventType_MouseLeave
                  HitBorders(*skinData)
                  DrawBorders(*skinData)
               Case #PB_EventType_LeftButtonUp
                  If \captureHit
                     \captureHit=0
                     DrawShadow(*skinData, 1)
                  EndIf
               Case #PB_EventType_LeftButtonDown
                  If HitBorders(*skinData)
                     \captureHit=\currentHit
                     \captX=WindowMouseX(win)
                     \captY=WindowMouseY(win)
                     rect\left=WindowX(win)
                     rect\top=WindowY(win)
                     rect\right=rect\left + WindowWidth(win)
                     rect\bottom=rect\top + WindowHeight(win)
                  EndIf                  
               Case #PB_EventType_MouseMove
                  If GetGadgetAttribute(gadget, #PB_Canvas_Buttons) & #PB_Canvas_LeftButton
                     If \captureHit & #ANY_BORDERS
                        Protected x=#PB_Ignore, y=#PB_Ignore, w=#PB_Ignore, h=#PB_Ignore
                        If \captureHit & #LEFT_BORDER
                           rect\left=DesktopMouseX() - \captX : w=rect\right-rect\left
                           x=rect\left
                        EndIf
                        If \captureHit & #RIGHT_BORDER
                           w=(rect\right - rect\left) + (DesktopMouseX() - \captX-rect\left)
                        EndIf
                        If \captureHit & #UP_BORDER
                           rect\top=DesktopMouseY() - \captY : h=rect\bottom-rect\top
                           y=rect\top
                        EndIf
                        If \captureHit & #DOWN_BORDER
                           h=(rect\bottom-rect\top) + (DesktopMouseY() - \captY-rect\top)
                        EndIf
                        DrawShadow(*skinData, 0)
                        ResizeWindow(win, x, y, w, h)
                        ResizeBorders(*skinData)
                        DrawBorders(*skinData)
                        ProcedureReturn
                     ElseIf \captureHit & #ANY_CAPTION
                        ResizeWindow(win, DesktopMouseX() - \captX, DesktopMouseY() - \captY, #PB_Ignore, #PB_Ignore)
                        ProcedureReturn
                     EndIf
                  EndIf
                  HitBorders(*skinData)
                  If \currentHit<>\previousHit
                     \previousHit=\currentHit
                     DrawBorders(*skinData)
                  EndIf
            EndSelect
         EndIf
      EndWith
   EndProcedure
   
   Procedure InitSkin()
      ; default colors
      SkinColors("background")=$7C7C7C
      SkinColors("border")=$6C6C6C
      SkinColors("border_inactive")=$6C6C6C
      SkinColors("button_close")=RGBA(230, 0, 0, 255)
      SkinColors("button")=RGBA(0, 0, 0, 0)
      SkinColors("button_inactive")=RGBA(0, 0, 0, 0)
      SkinColors("button_hover")=RGBA(0, 0, 0, 30)
      SkinColors("button_click")=RGBA(0, 0, 0, 50)
      SkinColors("stroke")=RGBA(0, 0, 0, 255)
      SkinColors("stroke_inactive")=RGBA(0, 0, 0, 100)
      SkinColors("stroke_hover")=RGBA(255, 255, 255, 255)
      SkinColors("stroke_click")=RGBA(255, 255, 255, 255)
      ; default sizes
      SkinSizes("border")=10
      SkinSizes("corner")=8
      SkinSizes("caption")=30
      ; default border items
      Protected BorderSize=SkinSizes("border"), CornerSize=SkinSizes("corner"), CaptionSize=SkinSizes("caption")
      AddBorderItem("up", "caption", 0, BorderSize, -BorderSize-1, CaptionSize-1)
      AddBorderItem("up", "title", 0, BorderSize, -45*3-BorderSize-5, CaptionSize-1)
      AddBorderItem("up", "close", 0, -45-BorderSize, -BorderSize-1, CaptionSize-1)
      AddBorderItem("up", "max", 0, -45*2-BorderSize, -45-BorderSize-1, CaptionSize-1)
      AddBorderItem("up", "min", 0, -45*3-BorderSize, -45*2-BorderSize-1, CaptionSize-1)
   EndProcedure
   
   Procedure SkinWindow(Window, IsResizable=#True)
      AddMapElement(SkinData(), "" + Window)
      With SkinData()
         \window=Window
         \isResizable=IsResizable
         \isActive=#True
         ; Skin borders
         \upBorder=CanvasGadget(#PB_Any, 0, 0, 0, 0)
         \leftBorder=CanvasGadget(#PB_Any, 0, 0, 0, 0)
         \rightBorder=CanvasGadget(#PB_Any, 0, 0, 0, 0)
         \downBorder=CanvasGadget(#PB_Any, 0, 0, 0, 0)
         ResizeBorders(SkinData())
         ; Skin events
         BindEvent(#PB_Event_ActivateWindow, @SkinEvents(), Window)
         BindEvent(#PB_Event_DeactivateWindow, @SkinEvents(), Window)
         BindEvent(#PB_Event_Gadget, @SkinEvents(), Window, \upBorder)
         BindEvent(#PB_Event_Gadget, @SkinEvents(), Window, \downBorder)
         BindEvent(#PB_Event_Gadget, @SkinEvents(), Window, \leftBorder)
         BindEvent(#PB_Event_Gadget, @SkinEvents(), Window, \rightBorder)
         ; Skin drawing (background, borders and shadow)
         SetWindowColor(Window, SkinColors("background"))
         DrawBorders(SkinData())
         DrawShadow(SkinData())
      EndWith
   EndProcedure
EndModule


CompilerIf #PB_Compiler_IsMainFile
   ; ********************
   ; EXAMPLE
   ; ********************
   UseModule SkinnableWindow
   
   InitSkin()
   If OpenWindow(10, 600, 600, 320, 240, "Borderless Window", #PB_Window_BorderLess)      
      SkinWindow(10, #True)
      ButtonGadget(100, 20, 50, 50, 20, "Exit")
      ButtonGadget(200, 80, 50, 50, 20, "Min")
      
      OpenWindow(11, 300, 300, 320, 240, "Borderless Window2", #PB_Window_BorderLess)
      SkinWindow(11, #True)
      
      Repeat
         Define event=WaitWindowEvent()
         If event=#PB_Event_Gadget
            Select EventGadget()
               Case 100
                  Break
                  
               Case 200
                  SetWindowState(10, #PB_Window_Minimize)
                  
            EndSelect
         EndIf
      Until event=#PB_Event_CloseWindow   
   EndIf
CompilerEndIf

Re: Skinnable Window

Posted: Thu Feb 09, 2017 2:17 pm
by Kwai chang caine
Really nice and modern window
Works very well, with no flickering on W7 X86 v5.60
Thanks for sharing 8)

Re: Skinnable Window

Posted: Fri Feb 10, 2017 6:29 am
by eddy
Image

[updates
- display button
- detect if window is active or not

Re: Skinnable Window (buttons, draggable, resizable)

Posted: Wed Jun 13, 2018 7:07 am
by Bisonte
Louise wrote:Hello
I have two questions :
1- How can I create a Title-Text for Skinnable window?
2. How can I define the event for window buttons? Like closing button and minimize.
Thanks.
2. :
The procedure "SkinEvents()" ... add the following at the end of the "Case #PB_EventType_LeftButtonDown" section.

Code: Select all

                   If \currentHit&#BUTTON_CLOSE
                     PostEvent(#PB_Event_CloseWindow, \window, \window)  
                   ElseIf \currentHit&#BUTTON_MAX
                     PostEvent(#PB_Event_MaximizeWindow, \window, \window)  
                   ElseIf \currentHit&#BUTTON_MIN
                     PostEvent(#PB_Event_MinimizeWindow, \window, \window)  
                   EndIf
and don't forget the event loop....here in eddy's demo code :

Code: Select all

If event = #PB_Event_MinimizeWindow
  SetWindowState(EventWindow(), #PB_Window_Minimize)  
EndIf
1 :
See at the code, where the Canvas is drawn... there you have to do "DrawText" or "DrawVectorText"
depended on the drawing mode....