Hi Jose,
I'd like to ask your opinion... Over the years I've had many people wanting to be able to set the foreground and background colors for the standard pushbutton that FireFly used as a control. Given that it is a standard Windows control that didn't allow user specified backcolors, I usually pointed them in the direction of other user created custom controls. For the new designer, I am thinking of just using your CXpButton as the one and only push button for the designer. What do you think? It would make things so much easier and, more importantly, less confusing and more user friendly for novices. If this path is chosen, the only thing that I think would be needed is to modify the control so that the user can specify fore/back colors (when themes are disabled for the control).
Sound good???
I wrote it many years ago (XP times) as an exercise of how to write a custom control using visual styles. Today it is not longer needed since you can use image lists with buttons, and I translated it to FreeBasic mainly to practice to write custom controls using a class. I will leave it as it is (you don't need to include it in the designer) and write another with a different name (XP is already vintage) based on it to save coding, with all the changes that you like. I personally dislike coloured buttons, but it is not my business if other people like them.
Awesome, thanks Jose! From using the control last night it seemed like the only things missing that really jumped out at me were the fore/back colors and the ability to use any type of image (not just icons and bitmaps) (png, jpg, etc) and to be able to set their transparency backcolor easily. Maybe you could integrate your AfxGdipIconFromRes function, etc.
PNGs and othe formats can already be used, e.g. instead of using LoadIcon you can use
pXpButton1.SetIcon AfxGdipImageFromFile(ExePath & "/Shutdown_48.png"), XPBI_NORMAL
or other GdiPlus wrappers.
Methods to add support for other image formats. The optional parameter dimPercent canbe used to dim the imape and in combination with bGrayScale to get disabled gray images from a normal image, e.g.:
pXpButton1.SetImageFromFile(ExePath & "/Shutdown_48.png", XPBI_NORMAL)
' Reuse the normal image to get a grayed image
pXpButton1.SetImageFromFile(ExePath & "/Shutdown_48.png", XPBI_DISABLED, TRUE, 60, TRUE)
' ========================================================================================
' Loads an image from file and sets it as the image of the button.
' - wszPath = Full path of the bitmap file.
' - ImageState =
' XPBI_NORMAL = 1
' XPBI_HOT = 2
' XPBI_DISABLED = 3
' - fRedraw = TRUE or FALSE (redraws the button to reflect the changes)
' - dimPercent = Percent of dimming (1-99)
' - bGrayScale = TRUE or FALSE. Convert to gray scale.
' ========================================================================================
' ========================================================================================
'PRIVATE FUNCTION AfxGdipIconFromFile (BYREF wszFileName AS WSTRING, _
' BYVAL dimPercent AS LONG = 0, BYVAL bGrayScale AS LONG = FALSE) AS HANDLE
' FUNCTION = AfxGdipImageFromFile(wszFileName, dimPercent, bGrayScale, IMAGE_ICON, 0)
'END FUNCTION
' ========================================================================================
' Loads an image from a resource file and sets it as the image of the button.
' - hInstance = [in] A handle to the module whose portable executable file or an accompanying
' MUI file contains the resource. If this parameter is NULL, the function searches
' the module used to create the current process.
' - wszImageName = [in] Name of the image in the resource file (.RES). If the image resource uses
' an integral identifier, wszImage should begin with a number symbol (#)
' followed by the identifier in an ASCII format, e.g., "#998". Otherwise,
' use the text identifier name for the image. Only images embedded as raw data
' (type RCDATA) are valid. These must be icons in format .png, .jpg, .gif, .tiff.
' - ImageState =
' XPBI_NORMAL = 1
' XPBI_HOT = 2
' XPBI_DISABLED = 3
' - fRedraw = TRUE or FALSE (redraws the button to reflect the changes)
' - dimPercent = Percent of dimming (1-99)
' - bGrayScale = TRUE or FALSE. Convert to gray scale.
' ========================================================================================
PRIVATE SUB CXpButton.SetImageFromRes (BYVAL hInstance AS HINSTANCE, BYREF wszImageName AS WSTRING, BYVAL ImageState AS LONG = XPBI_NORMAL, BYVAL fRedraw AS LONG = FALSE, _
BYVAL dimPercent AS LONG = 0, BYVAL bGrayScale AS LONG = FALSE)
DIM hIcon AS HICON = AfxGdipIconFromRes(hInstance, wszImageName, dimPercent, bGrayScale)
this.SetIcon(hIcon, ImageState, fRedraw)
END SUB
' ========================================================================================
Tip:
To solve the problem of different icon sizes depending of the DPI setting, you could add fields in the property list for the wanted width and height of the icon and, in the generated code, use:
pXpButton.SetImageSize pWindow.ScaleX(width), pWindow.ScaleY(height)
This way, the icon will we draw with the same relative size.
Quote from: José Roca on June 16, 2018, 01:14:11 PM
Tip:
To solve the problem of different icon sizes depending of the DPI setting, you could add fields in the property list for the wanted width and height of the icon and, in the generated code, use:
pXpButton.SetImageSize pWindow.ScaleX(width), pWindow.ScaleY(height)
This way, the icon will we draw with the same relative size.
Sounds good. I will have those settings in the designer for sure. How about icon transparency... for example, when you use your gdi functions to load a png from the resource file and convert them to icon format would we lose and background transparency? Could, say, the topmost left pixel become the color used for transparency? It would be very versatile if you can load images of any type from the resource file and display in the button such that it blends nicely with whatever color the button is currently using as a background.
PNG icons support transparency. Therefore you only need to use SetImageFromFile or SetImageFromRes. Nothing else. Gone are the times of bitmaps with a pink background :)
They also support the alpha channel.
What I can't get is to change the background color permanently. Since the control is not ownerdraw, it does not receive the WM_CTLCOLORBTN message and Windows reverts to the system colors as soon at it processes DefWindowProcW. You may need a different control.
I will add the new SetImage functions to the original CXpButton as an improvement.
Would forcing a setting where the control does not check for m_bIsThemed (basically set it be false) and then set properties to replace calls like GetSysColorBrush(COLOR_BTNFACE) to our own defined colors. Basically, ensure that the control draws using the GDI sections of the CXpButton.UxDrawPushButton function rather than the theme portions? Would that work?
I have tried that and the color changes when you click the button, but it reverts to system colors when you release it.
Quote from: José Roca on June 16, 2018, 05:23:32 PM
I have tried that and the color changes when you click the button, but it reverts to system colors when you release it.
Ah, yes, I see what you mean. I just tried it as well. Looks like it is the DrawFrameControl api that is the culprit. It uses its own colors.
uState = DFCS_BUTTONPUSH
IF iStateId = PBS_HOT THEN uState = uState OR DFCS_HOT
IF (lStyle AND BS_FLAT) = BS_FLAT THEN uState = uState OR DFCS_FLAT
.DrawFrameControl hDc, @rcContent, DFC_BUTTON, uState
Like you said, looks like this will entail having to go full ownerdrawn which is too bad.
But changing the order seems to work. I will try and post a modified file.
It works. Updated file and an example.
A further modification to get it working when the button is toggled.
' // Draws the button
IF m_bIsThemed THEN
' // Increase 1 pixel to include the edge
.InflateRect @rc, 1, 1
' // Draws the theme-specified border and fills for the "iPartId" and "iStateId".
.DrawThemeBackground(hTheme, hDc, BP_PUSHBUTTON, iStateId, @rc, NULL)
' // Gets the size of the content for the theme-defined background
.GetThemeBackgroundContentRect(hTheme, hDc, BP_PUSHBUTTON, iStateId, @rc, @rcContent)
ELSE
' // Uses GDI to draw the button
rcContent = rc
IF bIsFocused THEN
IF m_bIsToggle = FALSE OR bIsPressed = FALSE THEN
.FrameRect hDc, @rcContent, GetSysColorBrush(COLOR_WINDOWTEXT)
END IF
.InflateRect @rcContent, -1, -1
END IF
IF m_bIsToggle THEN
IF iStateId = PBS_PRESSED THEN
.DrawEdge hDc, @rcContent, EDGE_SUNKEN, BF_RECT OR BF_MIDDLE OR BF_SOFT
ELSE
IF (lStyle AND BS_FLAT) = BS_FLAT THEN
.DrawEdge hDc, @rcContent, EDGE_RAISED, BF_RECT OR BF_MIDDLE OR BF_SOFT OR BF_FLAT
ELSE
.DrawEdge hDc, @rcContent, EDGE_RAISED, BF_RECT OR BF_MIDDLE OR BF_SOFT
END IF
END IF
ELSE
IF bIsPressed THEN
.FrameRect hDc, @rcContent, GetSysColorBrush(COLOR_BTNSHADOW)
ELSE
uState = DFCS_BUTTONPUSH
IF iStateId = PBS_HOT THEN uState = uState OR DFCS_HOT
IF (lStyle AND BS_FLAT) = BS_FLAT THEN uState = uState OR DFCS_FLAT
.DrawFrameControl hDc, @rcContent, DFC_BUTTON, uState
END IF
END IF
IF m_hBkgBrush = NULL THEN
.FillRect hDc, @rcContent, GetSysColorBrush(COLOR_BTNFACE)
ELSE
.FillRect hDc, @rcContent, m_hBkgBrush
END IF
END IF
What I have done is to move FillRect to the bottom.
Okay, I see what you've done... seems to work for as well. Only thing is that you don't get that visual depressed look when the button is pressed and then released.
As an aside: I was reading about theming and noticed that the control doesn't respond to WM_THEMECHANGED
case WM_THEMECHANGED:
if(pData->hTheme)
CloseThemeData(pData->hTheme);
pData->hTheme = OpenThemeData(hwnd, wszClass);
InvalidateRect(hwnd, NULL, TRUE);
return 0;
Quote from: Paul Squires on June 16, 2018, 06:41:02 PM
As an aside: I was reading about theming and noticed that the control doesn't respond to WM_THEMECHANGED
case WM_THEMECHANGED:
if(pData->hTheme)
CloseThemeData(pData->hTheme);
pData->hTheme = OpenThemeData(hwnd, wszClass);
InvalidateRect(hwnd, NULL, TRUE);
return 0;
It is not needed. I call OpenThemeData and CloseThemeData in UxDrawPushButton each time that the button is redrawn.
Ahhhhhh...... :)
Unfortunately, my studying for tonight now has to end as I have to go out for the night. :(
Good series of articles you've no doubt have already read :)
https://www.codeproject.com/Articles/620045/Custom-Controls-in-Win-API-Visual-Styles
The guy who wrote them also maintains the mCtl project:
http://www.mctrl.org/
Quote from: Paul Squires on June 16, 2018, 06:57:54 PM
Unfortunately, my studying for tonight now has to end as I have to go out for the night. :(
Good series of articles you've no doubt have already read :)
https://www.codeproject.com/Articles/620045/Custom-Controls-in-Win-API-Visual-Styles
The guy who wrote them also maintains the mCtl project:
http://www.mctrl.org/
But this guy still has not learned how to write DPI aware controls :) The mCtl controls are unusable with High DPI settings.
Quote from: Paul Squires on June 16, 2018, 06:39:32 PM
Okay, I see what you've done... seems to work for as well. Only thing is that you don't get that visual depressed look when the button is pressed and then released.
Well, I have modified the code again and we will only lose the depressed look when using custom colors.
' // Draws the button
IF m_bIsThemed THEN
' // Increase 1 pixel to include the edge
.InflateRect @rc, 1, 1
' // Draws the theme-specified border and fills for the "iPartId" and "iStateId".
.DrawThemeBackground(hTheme, hDc, BP_PUSHBUTTON, iStateId, @rc, NULL)
' // Gets the size of the content for the theme-defined background
.GetThemeBackgroundContentRect(hTheme, hDc, BP_PUSHBUTTON, iStateId, @rc, @rcContent)
ELSE
' // Uses GDI to draw the button
rcContent = rc
IF bIsFocused THEN
IF m_bIsToggle = FALSE OR bIsPressed = FALSE THEN
.FrameRect hDc, @rcContent, GetSysColorBrush(COLOR_WINDOWTEXT)
END IF
.InflateRect @rcContent, -1, -1
END IF
IF m_bIsToggle THEN
IF iStateId = PBS_PRESSED THEN
.DrawEdge hDc, @rcContent, EDGE_SUNKEN, BF_RECT OR BF_MIDDLE OR BF_SOFT
ELSE
IF (lStyle AND BS_FLAT) = BS_FLAT THEN
.DrawEdge hDc, @rcContent, EDGE_RAISED, BF_RECT OR BF_MIDDLE OR BF_SOFT OR BF_FLAT
ELSE
.DrawEdge hDc, @rcContent, EDGE_RAISED, BF_RECT OR BF_MIDDLE OR BF_SOFT
END IF
END IF
ELSE
IF m_hBkgBrush = NULL THEN .FillRect hDc, @rcContent, GetSysColorBrush(COLOR_BTNFACE)
IF bIsPressed THEN
.FrameRect hDc, @rcContent, GetSysColorBrush(COLOR_BTNSHADOW)
ELSE
uState = DFCS_BUTTONPUSH
IF iStateId = PBS_HOT THEN uState = uState OR DFCS_HOT
IF (lStyle AND BS_FLAT) = BS_FLAT THEN uState = uState OR DFCS_FLAT
.DrawFrameControl hDc, @rcContent, DFC_BUTTON, uState
END IF
END IF
IF m_hBkgBrush THEN.FillRect hDc, @rcContent, m_hBkgBrush
END IF
I don't know what more I can do unless I get the source code of these API functions an rewrite them.
I have added support for text foreground and background colors. Also some minor changes, e.g. SetBkgColor changed to SetButtonBkColor.
I also have added SetTextForeDownColor and SetTextBkDownColor to set the colors used when the button is down (pressed or toggled). This is a workaround to provide a clear indication that the button is pressed or toggled.
If you don't need more changes, we can simply rename it as CXpButton.inc, as the original. This way I don't need to change all the documentation of this control.
Thanks Jose, yes, rename this one as the original. I will use it in the designer and if I encounter any problems or suggestions for improvements then I will let you know. It seems like it has all of the properties I would need. Tomorrow, I will look at the button in Visual Studio 2017 to see how the fore/back color affects that button and themeing.
Thanks!
It is SetTextForeDownColor an appropriate name? What I want to mean is foreground color when the button is down (pressed).
Maybe, "SetTextForeColorDown" or "SetTextForeColorPressed" ?
or even, "SetForeColorTextDown" or "SetForeColorTextPressed" ?
Changed names to SetTextForeColorDown and SetTextBkColorDown and renamed the include file as the original: CXpButton.inc.
Hi Jose, I'm happy to report that I have been able to integrate the CXpButton into the visual designer as a replacement for the standard Windows button (so far, so good). I only really needed to add a call to subclass the control and an additional pointer in order to track the NEW/DELETE sequence for when I create and subsequently destroy the control. Working on integrating all of the CXpButton functionality into the PropertyList. Any issues/problems, I'll let you know. Thanks
Excellent. I always have wondered why one of the first questions that every novice asks is how to change the color of a button. The result can be awful if you choose colors using a theme and then the user chooses another theme. The user needs a gui that doesn't strain their eyes. I have seen things like red text against a light green background or dark blue text against a black background.
José,
You probably have changed your Rar tool to new version, with my installed winrar 3.93 i was not able to uncompress your last attachments.
It would better stay on zip or previous version rar for more compatibiity when sharing
thanks for your tremendous job
Same file in .zip format.
Also, don't forget that you can always get Jose's latest files from: https://github.com/JoseRoca/WinFBX
Should the CXpButton's TextMargin and ImageMargin be adjusted based on DPI setting? It says that the incoming value should be specified as a pixel number (the default is 4 pixels). Should that value be upscaled when the control is drawn? pWindow->ScaleX(m_TextMargin)
Well, when I wrote it, DPI did not still existed... and I had not realized this detail when I did the translation. Maybe we can modify the control to scale this value and also m_ImageWidth, m_ImageHeight and m_ImageMargin.
Thanks Jose, yes, I think the control should do the transformation of the ImageMargin and TextMargin values during the drawing routine. (not sure about the image scaling.. maybe let the user do that one in case they manually specify images based on dpi settings. Wouldn't want a 64px image to be scaled even bigger automatically).
Then we will scale only the margins.
Changed it to scale the margins.
Thanks Jose, I will integrate this new version into the visual designer.
Hi Jose, I am working on the various properties of the CXpButton. It would seem to me that it would be easier to work with the control if the various items were implemented as Properties rather than functions. There are various functions to set colors for the background and various text states but no functions to retrieve those values. If implemented as properties then the it would eliminate the need to have a corresponding Get function.
eg.
Using Function approach:
SetButtonBkColor
GetButtonBkColor (this function does not exist)
Using Property approach:
ButtonBkColor( byval nValue as COLORREF )
ButtonBkColor () as COLORREF
Is it too late in this control's life to make such a large change? If it is too late, then maybe you could simply add the missing Get functions for the sake of completeness?
Now you have both functions and properties:
' // Properties
DECLARE PROPERTY Font () AS HFONT
DECLARE PROPERTY Font (BYVAL hFont AS HFONT)
DECLARE PROPERTY TextFormat () AS DWORD
DECLARE PROPERTY TextFormat (BYVAL dwTextFlags AS DWORD)
DECLARE PROPERTY Cursor () AS HCURSOR
DECLARE PROPERTY Cursor (BYVAL hCursor AS HCURSOR)
DECLARE PROPERTY Toggle (BYVAL fToggle AS LONG)
DECLARE PROPERTY Toggle () AS LONG
DECLARE PROPERTY ToggleState () AS LONG
DECLARE PROPERTY ToggleState (BYVAL fState AS LONG)
DECLARE PROPERTY ImageWidth () AS LONG
DECLARE PROPERTY ImageWidth (BYVAL nWidth AS LONG)
DECLARE PROPERTY ImageHeight () AS LONG
DECLARE PROPERTY ImageHeight (BYVAL nHeight AS LONG)
DECLARE PROPERTY ImagePos () AS LONG
DECLARE PROPERTY ImagePos (BYVAL nPos AS LONG)
DECLARE PROPERTY ImageMargin () AS LONG
DECLARE PROPERTY ImageMargin (BYVAL nMargin AS LONG)
DECLARE PROPERTY ButtonState () AS LONG
DECLARE PROPERTY ImageType () AS LONG
DECLARE PROPERTY ButtonBkColor () AS COLORREF
DECLARE PROPERTY ButtonBkColor (BYVAL bkColor AS COLORREF)
DECLARE PROPERTY TextForeColor () AS COLORREF
DECLARE PROPERTY TextForeColor (BYVAL textColor AS COLORREF)
DECLARE PROPERTY TextBkColor () AS COLORREF
DECLARE PROPERTY TextBkColor (BYVAL textColor AS COLORREF)
DECLARE PROPERTY TextForeColorDown () AS COLORREF
DECLARE PROPERTY TextForeColorDown (BYVAL textColor AS COLORREF)
DECLARE PROPERTY TextBkColorDown () AS COLORREF
DECLARE PROPERTY TextBkColorDown (BYVAL textColor AS COLORREF)
DECLARE PROPERTY NormalImageHandle () AS HANDLE
DECLARE PROPERTY HotImageHandle () AS HANDLE
DECLARE PROPERTY DisabledImageHandle () AS HANDLE
DECLARE PROPERTY BkBrush () AS HBRUSH
Functions have the advantage of the additional fRedraw parameter. Using properties on the fly, you may need to call the Redraw method separately.
Thanks Jose! Great job as always :)
Here's another one....
Would be nice if CXpButton handled the WM_SETFONT message. I know that I can set it via the Font property but being able to WM_SETFONT to the control is easier based on the code I am using. WM_SETCURSOR is handled, but not WM_SETFONT.
This should work:
CASE WM_SETFONT
' // Sets the font that a control is to use when drawing text.
pButton = CAST(CXpButton PTR, .GetWindowLongPtrW(hwnd, 0))
IF pButton THEN
pButton->SetFont(CAST(.HFONT, wParam), lParam)
EXIT FUNCTION
END IF
Thanks Jose, yes it seems to be working perfectly so far.
Hi Jose,
Maybe add code for the button to handle an incoming BM_CLICK message? I am sending the BM_CLICK message to the button designated as the default button when the ENTER key or ESC keys are pressed. This works perfectly for regular push buttons of course, but fails with the CXPButton button because it doesn't handle the BM_CLICK message.
I have added:
CASE BM_CLICK
.SendMessageW hwnd, WM_LBUTTONDOWN, 0, 0
.SendMessageW hwnd, WM_LBUTTONUP, 0, 0
EXIT FUNCTION
Not tested yet, but it should work.
I doubt that will work because LBUTTONDOWN and LBUTTONUP do hit testing based on the cursor position to determine if the user has clicked the button. When I get home this evening I will work up some code and post it here for you to include in the button class.
Thanks, Paul
You're right. I had no time for testing it. Let's see if this works:
CASE BM_CLICK
IF .IsWindowEnabled(hwnd) = FALSE THEN EXIT FUNCTION
' // Redraws the button in pushed state
pButton = CAST(CXpButton PTR, .GetWindowLongPtrW(hwnd, 0))
IF pButton = NULL THEN EXIT FUNCTION
IF pButton->m_bIsToggle THEN
IF pButton->m_bToggled = FALSE THEN
pButton->m_bToggled = TRUE
ELSE
pButton->m_bToggled = FALSE
END IF
END IF
pButton->m_fState = BST_PUSHED
pButton->Redraw
' // Redraws the button in unpushed state
pButton->m_fState = 0
pButton->Redraw
' // Set the focus in the button
.SetFocus hwnd
' // Forwards the message to the parent window
.SendMessageW .GetParent(hwnd), WM_COMMAND, MAKELONG(GetDlgCtrlId(hwnd), BN_CLICKED), CAST(.LPARAM, hwnd)
EXIT FUNCTION
Thanks Jose! I tested it and it seems to be working okay so far :-)
Hi Paul,
I have added the ButtonBkColorDown properties to change the background color of the button when it is pressed or toggled.
Hi José,
Thanks for the new code. I added a new Button property called "BackColorDown". Seems to be working okay.
Hi José,
The CXpButton could use two functions similar to the ones that you have for the CImageCtx control. Currently, I have to use the GetWindowLongPtr calls directly during my WM_DESTROY to get the CXpButton pointers. For convenience and consistency it would be nice to have AfxCXpButtonPtr like the AfxCImageCtxPtr functions below.
' ========================================================================================
' Returns a pointer to the class given the handle of its associated window handle.
' hCtl = Handle of the image control.
' ========================================================================================
PRIVATE FUNCTION AfxCImageCtxPtr OVERLOAD (BYVAL hCtl AS HWND) AS CImageCtx PTR
FUNCTION = CAST(CImageCtx PTR, .GetWindowLongPtrW(hCtl, 0))
END FUNCTION
' ========================================================================================
' ========================================================================================
' hParent = Handle of the parent window of the image control.
' cID = Identifier of the image control.
' ========================================================================================
PRIVATE FUNCTION AfxCImageCtxPtr OVERLOAD (BYVAL hParent AS HWND, BYVAL cID AS LONG) AS CImageCtx PTR
FUNCTION = CAST(CImageCtx PTR, .GetWindowLongPtrW(GetDlgItem(hParent, cID), 0))
END FUNCTION
' ========================================================================================
Done.
I smile when I remember that I thought of deprecate it, and only adapted it to practice how to implement a custom control using a class :)
Excellent - thanks :-)
That button is one of the main controls in the WinFBE visual designer code output. It is very versatile.
Hi José,
I had to make a very change to the CTmageCtx.DrawImage function. I had to move the test for the pImage pointer being null down to the block where the image is actually outputted. It now looks like this:
IF ( pGraphics ) andalso ( pImage )THEN
This enables me to draw the background normal or HOT regardless of whether an image is actually specified.
I have attached the new CImageCtx control code.