Skinnamarink - a tutorial

Share your advanced PureBasic knowledge/code with the community.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Skinnamarink - a tutorial

Post by netmaestro »

Code updated For 5.20+

There are a couple of ways to approach the task of window skinning depending upon the OS you're targeting. If you're developing for Windows 2000/XP only, the layered window functions may be all you need. But what about Windows 9x? No layering is available there. For a skinning method that will work "everywhere" (on Windows, of course) there is the region clipping approach. How does this work? You begin by creating a region object that is the size and shape of the skin you want to use, set the window clipping region to it, and paint your image. That's basically all there is to it. Well, if it's so easy, why doesn't everyone do it? One reason is that some people don't like skins. Another is that even though the concept is simple, the implementation can be somewhat involved. Take the first step, for example:

Create a Region object the size and shape of your skinning image.

There are several Region creation functions, such as CreateRectRgn, CreateEllipticRgn, CreateRoundRectRgn, etc. These will allow you to create an irregular-shaped region, and together with CombineRgn can be used to create a region of any shape you desire. But the difficulty comes in when you need to create a region that is so complex it would take days of hard work to put it all together by hand. So - you need a way to scan an image, choose a transparent colour and create a complex region programmatically. Here is a little utility that will:

- Take a Bitmap image as input
- Let you choose a transparent colour
- Create the region data from the image
- Compress the image data
- Save both components in a custom skin file for later use

Here is an image to test with, but any 24-bit bitmap will do: http://www.networkmaestro.com/skintest.bmp

And here is the code for the skin creation utility:

Code: Select all

;==========================================================
; Project:         Skinnamarink (skinning tutorial)
; Program:         Custom Skin Creator
; Author:          netmaestro
; Date:            October 2, 2006
; Target OS:       Windows All
; Target Compiler: PureBasic 4.0 and above
; License:         Use as you wish, credit appreciated
;                  but not required
;==========================================================

Procedure ScanRegion(image)
  OpenWindow(0,0,0,ImageWidth(image),ImageHeight(image),"Click on Transparent Color",#PB_Window_ScreenCentered)
  CreateGadgetList(WindowID(0))
  ImageGadget(0,0,0,0,0,ImageID(image))
  quit = 0
  Repeat
    EventID = WaitWindowEvent()
    If EventID = #PB_Event_Gadget
      If EventGadget() = 0
        If EventType() = #PB_EventType_LeftClick
          StartDrawing(WindowOutput(0))
            transcolor = Point(WindowMouseX(0),WindowMouseY(0))
            SetWindowTitle(0, "Current Choice: RGB("+Str(Red(transcolor))+","+Str(Green(transcolor))+","+Str(Blue(transcolor))+")")
            result = MessageRequester("","Current Choice = RGB("+Str(Red(transcolor))+","+Str(Green(transcolor))+","+Str(Blue(transcolor))+"), accept?",#MB_YESNOCANCEL|$C0)
            Select result
              Case 6,2
              quit = 1
            EndSelect
          StopDrawing()
        EndIf
      EndIf
    EndIf
  Until quit
  CloseWindow(0)
  If result = 2 ; user pressed Cancel
    End
  EndIf
  OpenWindow(0,0,0,300,70,"Creating Skin, please wait...",#PB_Window_ScreenCentered)
  CreateGadgetList(WindowID(0))
  StickyWindow(0, 1)
  ProgressBarGadget(0, 0,20,300,20,0,ImageHeight(image),#PB_ProgressBar_Smooth)
  SetGadgetColor(0,#PB_Gadget_FrontColor,#Blue)
  hRgn = CreateRectRgn_(0,0,ImageWidth(image),ImageHeight(image))
  StartDrawing(ImageOutput(image))
    For y=0 To ImageHeight(image)
      For x=0 To ImageWidth(image)
        If Point(x,y) = transcolor
          hTmpRgn = CreateRectRgn_(x,y,x+1,y+1)
          CombineRgn_(hRgn, hRgn, hTmpRgn, #RGN_XOR)
          DeleteObject_(hTmpRgn);
        EndIf
      Next
      SetGadgetState(0,y)
    Next
  StopDrawing()
  CloseWindow(0)
  ProcedureReturn hRgn;
EndProcedure

;======================================
;    Load the image to be skinned
;======================================

filename$ = OpenFileRequester("Image File Conversion Utility",GetPathPart(GetCurrentDirectory()),"Bitmap |*.bmp",1)
hSkinBmp=LoadImage(#PB_Any, filename$)
If Not IsImage(hSkinBmp)
  MessageRequester("Error","Unable to load that image file!",#MB_ICONERROR)
  End
EndIf

;====================================================================
;    Compress and prepare the image data portion of the skin file
;====================================================================

GetObject_(ImageID(hSkinBmp), SizeOf(BITMAP), @Bmp.BITMAP)
ColorBytesSize = Bmp\bmWidthBytes * Bmp\bmHeight
ImageSize = SizeOf(BITMAPFILEHEADER) + SizeOf(BITMAPINFOHEADER) + ColorBytesSize 

*rawimage = AllocateMemory(ImageSize)
*fileheader.BITMAPFILEHEADER = *rawimage
*header.BITMAPINFOHEADER = *rawimage + SizeOf(BITMAPFILEHEADER)

With *fileheader
  \bfType = 19778 ; word containing 2 bytes, 'BM' for 'BIT' 'MAP' ;)
  \bfSize = ImageSize
  \bfOffBits = SizeOf(BITMAPFILEHEADER) + SizeOf(BITMAPINFOHEADER)
EndWith

With *header
  \biSize = SizeOf(BITMAPINFOHEADER)
  \biWidth = Bmp\bmWidth
  \biHeight = Bmp\bmHeight
  \biPlanes = 1
  \biBitCount = 24
  \biCompression = #BI_RGB
EndWith

CopyMemory(Bmp\bmBits, *rawimage + SizeOf(BITMAPFILEHEADER) + SizeOf(BITMAPINFOHEADER), ColorbytesSize)
*PackedImage = AllocateMemory(ImageSize+8)
 PackSize = CompressMemory(*rawimage, ImageSize, *PackedImage, ImageSize)

;=======================================================
;    Process the image data thru the Region Scanner
;=======================================================

hRegion = ScanRegion(hSkinBmp)
RegionSize = GetRegionData_(hRegion, 0, 0)
*RegionData = AllocateMemory(RegionSize)
GetRegionData_(hRegion,RegionSize,*RegionData)

;=====================================================================
;   Save compressed Imagedata and Regiondata in a custom Skin file
;=====================================================================

SaveFile$ = InputRequester("Choose a name for the Skin File","Name: ","test.skn")

If CreateFile(0, SaveFile$)
  WriteLong(0, ImageSize)
  WriteLong(0, packsize + 12) ; Offset of region data in skin file
  WriteLong(0, Regionsize) ; Length of region data in skin file
  WriteData(0, *PackedImage, PackSize)
  WriteData(0, *RegionData, RegionSize)
  CloseFile(0)
Else
  MessageRequester("Error","Unable to save the file!",#MB_ICONERROR)
EndIf

End
The structure of the custom skin file is as follows:

Code: Select all

Uncompressed image size:  4 bytes (1 long) 
Regiondata Offset:        4 bytes (1 long) 
Regiondata size:          4 bytes (1 long) 
Compressed imagedata:     (undetermined size) 
Compressed regiondata:    (undetermined size)
The next component is an includefile that contains the functions necessary to read the skin file and apply it to your window. Here it is:

Code: Select all

;==========================================================
; Project:         Skinnamarink (skinning tutorial)
; Program:         SkinWindow.pbi
; Author:          netmaestro
; Date:            October 2, 2006
; Target OS:       Windows All
; Target Compiler: PureBasic 4.0 and above
; License:         Use as you wish, credit appreciated
;                  but not required
;==========================================================

Procedure SkinProc(hwnd, msg, wparam, lparam)
  Shared oldskinproc, BGBrush
  result = CallWindowProc_(oldskinproc, hwnd, msg, wparam, lparam)
  Select msg
    Case #WM_LBUTTONDOWN
      SendMessage_(hwnd, #WM_NCLBUTTONDOWN, #HTCAPTION, 0)
    Case #WM_DESTROY
      DeleteObject_(BGBrush)
  EndSelect
  ProcedureReturn Result
EndProcedure

Procedure SkinWindow(hwnd, loc)
  Shared oldskinproc, BGBrush
  unpackedsize = PeekL(loc)
  RegionOffset = PeekL(loc+4)
  RegionSize = PeekL(loc + 8)  
  packedsize = RegionOffset - 12
  *unpacked = AllocateMemory(unpackedsize)
  UncompressMemory(loc + 12,packedsize, *unpacked,unpackedsize)
  Skin = CatchImage(#PB_Any, *unpacked)
  FreeMemory(*unpacked)
  hRegion = ExtCreateRegion_(0, RegionSize, loc + RegionOffset)
  SetWindowRgn_(hWnd, hRegion, #True);
  BGBrush = CreatePatternBrush_(ImageID(Skin))
  FreeImage(Skin)
  SetClassLong_(hwnd, #GCL_HBRBACKGROUND, BGBrush)
  InvalidateRect_(hwnd,0,1)
  DeleteObject_(hRegion1)
  oldskinproc = SetWindowLong_(hwnd, #GWL_WNDPROC, @SkinProc())
  SetWindowPos_(hwnd, #HWND_TOPMOST,0,0,0,0,#SWP_NOSIZE|#SWP_NOMOVE)
EndProcedure
The SkinWindow() procedure unpacks the image and region from the custom skin file you've included in the executable with IncludeBinary. It goes on from there to subclass the window so that it will respond to mousemove events and clean up after itself on closing. Because SetClassLong with a background brush is employed, no #WM_PAINT messages need be handled. Windows manages this all on its own because the background brush is applied. All that remains now is a little example of how to use these tools:

Code: Select all

IncludeFile "SkinWindow.pbi"

hWnd=OpenWindow(0,0,0,600,600,"",#PB_Window_ScreenCentered|#PB_Window_BorderLess)
CreateGadgetList(WindowID(0))
ButtonGadget(0,26,26,40,20,"Close")
SkinWindow(WindowID(0), ?skin)

quit = 0
Repeat
  EventID = WaitWindowEvent()
  Select EventID
    Case #PB_Event_Gadget
      quit = 1
  EndSelect
Until quit

DataSection
  skin: IncludeBinary "test.skn"
EndDataSection
That's it. There's no more to it than that. Run the Creation tool on the test BMP to start with, run the test program and see how it works for you. Then, you can go on and skin up any BMP of your choice and apply it to your window with almost no effort at all.
Last edited by netmaestro on Tue Oct 03, 2006 9:21 am, edited 1 time in total.
BERESHEIT
danraymond
User
User
Posts: 43
Joined: Wed Jun 28, 2006 6:02 am

Fabulous!!!!!

Post by danraymond »

You must have been reading my mind!

thanks Netmaestro for another outstanding contributon

Dan Raymond
User avatar
Fangbeast
PureBasic Protozoa
PureBasic Protozoa
Posts: 4789
Joined: Fri Apr 25, 2003 3:08 pm
Location: Not Sydney!!! (Bad water, no goats)

Master netmaestro

Post by Fangbeast »

You are a coding fiend!!!

/me bows to your munificense!
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post by netmaestro »

Code updated For 5.20+
muni - wait a minute while I look that up... ok.. not an insult... :lol:

Here's a more advanced version of the Skin Creator, it contains a couple of enhancements:

- The program will now take PNG or TIFF images as input and convert them to 24-bit BMPs for use as a skin. JPEGs are not supported and never will be due to their 'non-lossless' compression. They are unsuitable for skins.

- You can now enter a tolerance level for the region scanner. This may help to create a cleaner skin if you have antialiasing around your image which results in pixels very close to the transparent color but not quite.

I didn't update the first post with this code as I want it to read as simply as possible. The concepts are not deep but they can be harder to assimilate if there's a lot of extra features tacked on.

Code: Select all

    ;==========================================================
    ; Project:         Skinnamarink (skinning tutorial)
    ; Program:         Custom Skin Creator
    ; Author:          netmaestro
    ; Date:            October 2, 2006
    ; Target OS:       Windows All
    ; Target Compiler: PureBasic 4.0 and above
    ; License:         Use as you wish, credit appreciated
    ;                  but not required
    ;==========================================================
    Global BmiInfo.BITMAPINFOHEADER, bmpWidth.w, bmpHeight.w

    UsePNGImageDecoder()
    UseTIFFImageDecoder()
    ; never use JPEGs for skins... you'll be sorry if you try...

    Procedure Get24BitColors(pBitmap)
      GetObject_(pBitmap, SizeOf(BITMAP), @bmp.BITMAP)
      *pPixels = AllocateMemory(bmpWidth*bmpHeight*3)
      hDC = GetWindowDC_(#Null)
      iRes = GetDIBits_(hDC, pBitmap, 0, bmpHeight, *pPixels, @bmiInfo, #DIB_RGB_COLORS)
      ReleaseDC_(#Null, hDC)
      ProcedureReturn *pPixels
    EndProcedure

    Procedure ScanRegion(image)
      OpenWindow(0,0,0,bmpWidth,bmpHeight,"Click on Transparent Color",#PB_Window_ScreenCentered)

      ImageGadget(0,0,0,0,0,ImageID(image))
      quit = 0
      Repeat
        EventID = WaitWindowEvent()
        If EventID = #PB_Event_Gadget
          If EventGadget() = 0
            If EventType() = #PB_EventType_LeftClick
              StartDrawing(WindowOutput(0))
                transcolor = Point(WindowMouseX(0),WindowMouseY(0))
                SetWindowTitle(0, "Current Choice: RGB("+Str(Red(transcolor))+","+Str(Green(transcolor))+","+Str(Blue(transcolor))+")")
                result = MessageRequester("","Current Choice = RGB("+Str(Red(transcolor))+","+Str(Green(transcolor))+","+Str(Blue(transcolor))+"), accept?",#MB_YESNOCANCEL|$C0)
                Select result
                  Case 6,2
                  quit = 1
                EndSelect
              StopDrawing()
            EndIf
          EndIf
        EndIf
      Until quit
      CloseWindow(0)
      If result = 2 ; user pressed Cancel
        End
      EndIf
      tolerance$ = "99"
      While Val(tolerance$) < 0 Or Val(tolerance$) > 30
        tolerance$ = InputRequester("Enter Tolerance level","Tolerance (0-30): ","")
      Wend
      tolerance = Val(tolerance$)
      OpenWindow(0,0,0,300,70,"Creating Skin, please wait...",#PB_Window_ScreenCentered)

      StickyWindow(0, 1)
      ProgressBarGadget(0, 0,20,300,20,0,ImageHeight(image),#PB_ProgressBar_Smooth)
      SetGadgetColor(0,#PB_Gadget_FrontColor,#Blue)
      hRgn = CreateRectRgn_(0, 0, bmpWidth, bmpHeight)
      StartDrawing(ImageOutput(image))
        For y=0 To ImageHeight(image)
          For x=0 To ImageWidth(image)
            color = Point(x,y)
            If Abs(Red(color)-Red(transcolor)) <= tolerance
              If Abs(Green(color)-Green(transcolor)) <= tolerance
                If Abs(Green(color)-Green(transcolor)) <= tolerance
                  hTmpRgn = CreateRectRgn_(x,y,x+1,y+1)
                  CombineRgn_(hRgn, hRgn, hTmpRgn, #RGN_XOR)
                  DeleteObject_(hTmpRgn)
                EndIf
              EndIf
            EndIf
          Next
          SetGadgetState(0,y)
        Next
      StopDrawing()
      CloseWindow(0)
      ProcedureReturn hRgn;
    EndProcedure

    ;======================================
    ;    Load the image to be skinned
    ;======================================

    Pattern$ = "PNG (*.png)|*.png|TIFF (*.tif)|*.tif|BMP (*.bmp)|*.bmp"
    filename$ = OpenFileRequester("Image File Conversion Utility",GetPathPart(GetCurrentDirectory()), Pattern$, 1)

    If filename$ = ""
      MessageRequester("Notice","Canceled by user",#MB_ICONINFORMATION)
      End
    EndIf

    hSkinBmp=LoadImage(#PB_Any, filename$)
    If Not IsImage(hSkinBmp)
      MessageRequester("Error","Unable to load that image file!", #MB_ICONERROR)
      End
    EndIf

    ;====================================================================
    ;    Compress and prepare the image data portion of the skin file
    ;====================================================================

    GetObject_(ImageID(hSkinBmp), SizeOf(BITMAP), @Bmp.BITMAP)

    bmpWidth  = bmp\bmWidth
    bmpWidth  - (bmpWidth % 4)                 
    bmpHeight = bmp\bmHeight

    With BmiInfo
      \biSize         = SizeOf(BITMAPINFOHEADER)
      \biWidth        = bmpWidth
      \biHeight       = bmpHeight
      \biPlanes       = 1
      \biBitCount     = 24
      \biCompression  = #BI_RGB 
    EndWith

    *_24bitColors = Get24BitColors(ImageID(hSkinBmp))

    ImageSize = SizeOf(BITMAPFILEHEADER) + SizeOf(BITMAPINFOHEADER) +  MemorySize(*_24bitColors)

    *rawimage = AllocateMemory(ImageSize)
    *fileheader.BITMAPFILEHEADER = *rawimage
    *header.BITMAPINFOHEADER = *rawimage + SizeOf(BITMAPFILEHEADER)

    With *fileheader
      \bfType = 19778 ; word containing 2 bytes, 'BM' for 'BIT' 'MAP' ;)
      \bfSize = ImageSize
      \bfOffBits = SizeOf(BITMAPFILEHEADER) + SizeOf(BITMAPINFOHEADER)
    EndWith

    CopyMemory(@BmiInfo, *header, SizeOf(BITMAPINFOHEADER))

    CopyMemory(*_24bitColors, *rawimage + SizeOf(BITMAPFILEHEADER) + SizeOf(BITMAPINFOHEADER), MemorySize(*_24bitColors))
    *PackedImage = AllocateMemory(ImageSize+8)
    PackSize =CompressMemory(*rawimage, ImageSize, *PackedImage, ImageSize)
    

    ;=======================================================
    ;    Process the image data thru the Region Scanner
    ;=======================================================

    hRegion = ScanRegion(hSkinBmp)
    RegionSize = GetRegionData_(hRegion, 0, 0)
    If Regionsize
      *RegionData = AllocateMemory(RegionSize)
      GetRegionData_(hRegion,RegionSize,*RegionData)
    Else
      MessageRequester("Error","Unable to create Region data!",#MB_ICONERROR)
      End
    EndIf

    ;=====================================================================
    ;   Save compressed Imagedata and Regiondata in a custom Skin file
    ;=====================================================================

    SaveFile$ = InputRequester("Choose a name for the Skin File","Name: ","test.skn")

    If CreateFile(0, SaveFile$)
      WriteLong(0, ImageSize)
      WriteLong(0, packsize + 12) ; Offset of region data in skin file
      WriteLong(0, Regionsize) ; Length of region data in skin file
      WriteData(0, *PackedImage, PackSize)
      WriteData(0, *RegionData, RegionSize)
      CloseFile(0)
    Else
      MessageRequester("Error","Unable to save the file!",#MB_ICONERROR)
    EndIf

    End

BERESHEIT
User avatar
Blue
Addict
Addict
Posts: 964
Joined: Fri Oct 06, 2006 4:41 am
Location: Canada

Post by Blue »

Wow. What a great tutorial.
I love it.

Thanks a lot.

(I ran the test with the picture you provided.
And I found that we can get a 2nd interesting window by simply removing the borderless property...)
real
User
User
Posts: 49
Joined: Fri Oct 08, 2004 5:17 am

Post by real »

Did somebody try to create custom gadgets?
Post Reply