Windows API: Why does button hover effect gets jammed after a while? - c++

Having recently started developing with the Windows API, I decided some interactivity was in order. So the plan was to use Owner Drawn buttons that would change background colour when the mouse enters their rectangular area, and changes back to the original colour when the mouse leaves.
I made 3 such buttons and sub-classed them with their own custom window procedure. Now because there is really no such thing as a WM_MOUSEENTER message I used the WM_MOUSEMOVE and some mouse tracking to Fill the window with a certain colour. This basically means the rectangle gets filled every time the cursor moves inside the window.
I also used WM_MOUSELEAVE to fill the button when the mouse pointer moves outside the button's rectangle.
And this seems to work fine at first but after a while the rectangle filling will stop and the background will be stuck in either the hover colour or the default colour.
Here's the Code
#ifndef UWMENU_H_INCLUDED
#define UWMENU_H_INCLUDED
#endif // UWMENU_H_INCLUDED
#include <iostream>
using namespace std;
LRESULT CALLBACK DBWndProc(HWND, UINT, WPARAM, LPARAM);
WNDPROC DBWndProcOld[3];
class UWMenu{
public:
//WINDOW PROCEDURE FOR NEWS BUTTON
static LRESULT CALLBACK DBWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam){
HBRUSH BG_btnOne = CreateSolidBrush(NAV_BTN_DEF_CLR);
HBRUSH BG_btnOneOver = CreateSolidBrush(NAV_BTN_OVER_CLR);
int btnID = GetDlgCtrlID(hwnd);
cout << btnID << "\n";
switch(msg){
case WM_MOUSEMOVE:{
cout << "Mouse is hovering over button\n";
SetCursor(LoadCursor(NULL, IDC_HAND));
HDC hdc = GetDC(hwnd);
RECT rc;
rc.left = 0; //Top left X coordinate of button's rectangular area
rc.top = 0; //Top left Y coordinate of button's rectangular area
rc.right = NAV_BTN_WIDTH; //Bottom right X coordinate of button's rectangular area
rc.bottom = NAV_BTN_HEIGHT; //Bottom right Y coordinate of button's rectangular area
printf("Repainting button background color\n");
FillRect(hdc, &rc, BG_btnOneOver);
SetTextColor(hdc, RGB(255,255,255));
SetBkMode(hdc, TRANSPARENT);
if(btnID == NBID_NEWS){DrawText(hdc, TXT_BN_NEWS, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);}
if(btnID == NBID_TOOLS){DrawText(hdc, TXT_BN_TOOLS, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);}
if(btnID == NBID_EDITOR){DrawText(hdc, TXT_BN_EDITORS, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);}
ReleaseDC(hwnd, hdc);
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(TRACKMOUSEEVENT);
tme.dwFlags = TME_LEAVE;
tme.hwndTrack = hwnd;
tme.dwHoverTime = HOVER_DEFAULT;
TrackMouseEvent(&tme);
}
break;
case WM_MOUSELEAVE:{
printf("You left me\n");
SetCursor(LoadCursor(NULL, IDC_ARROW));
HDC hdc = GetDC(hwnd);
RECT rc;
rc.left = 0; //Top left X coordinate of button's rectangular area
rc.top = 0; //Top left Y coordinate of button's rectangular area
rc.right = NAV_BTN_WIDTH; //Bottom right X coordinate of button's rectangular area
rc.bottom = NAV_BTN_HEIGHT; //Bottom right Y coordinate of button's rectangular area
FillRect(hdc, &rc, BG_btnOne);
SetTextColor(hdc, RGB(255,255,255));
SetBkMode(hdc, TRANSPARENT);
if(btnID == NBID_NEWS){DrawText(hdc, TXT_BN_NEWS, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);}
if(btnID == NBID_TOOLS){DrawText(hdc, TXT_BN_TOOLS, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);}
if(btnID == NBID_EDITOR){DrawText(hdc, TXT_BN_EDITORS, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);}
ReleaseDC(hwnd, hdc);
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(TRACKMOUSEEVENT);
tme.dwFlags = TME_CANCEL;
tme.hwndTrack = hwnd;
tme.dwHoverTime = HOVER_DEFAULT;
TrackMouseEvent(&tme);
}
break;
}
return CallWindowProc(DBWndProcOld[btnID-200], hwnd, msg, wParam, lParam);
}
};
The mouse_move messages are being processed as the output from printf() and cout are shown in debug mode when the mouse moves over the buttons.
Is there any way to fix this? Something to do with mouse tracking perhaps?
I think the problem is that the button is sort of getting spammed with the FillRect() while the pointer is inside its rectangle.
Any help is appreciated.

This problem has been solved now. It was due to a GDI leak. The problem was solved by deleting brushes that were being created repeatedly in a parent window procedure.

Related

C++ winapi Taking Screenshot and Making It Background of Window

I'm trying to make the snippingtool in c++. I managed to create a borderless, fullscreen window via this code;
WindProc:
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
case WM_CHAR: //this is just for a program exit besides window's borders/taskbar
if (wparam==VK_ESCAPE)
{
DestroyWindow(hwnd);
}
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, message, wparam, lparam);
}
}
Creating the window;
WNDCLASS windowClass={0};
windowClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
windowClass.hCursor=LoadCursor(NULL, IDC_ARROW);
windowClass.hInstance=NULL;
windowClass.lpfnWndProc=WndProc;
windowClass.lpszClassName=TEXT("Window in Console"); //needs to be the same name
//when creating the window as well
windowClass.style=CS_HREDRAW | CS_VREDRAW;
//also register the class
if (!RegisterClass(&windowClass))
MessageBoxA(NULL, "Could not register class", "Error", MB_OK);
HWND windowHandle=CreateWindowA("Window in Console",
NULL,
WS_POPUP, //borderless
0, //x coordinate of window start point
0, //y start point
GetSystemMetrics(SM_CXSCREEN), //width of window
GetSystemMetrics(SM_CYSCREEN), //height of the window
NULL, //handles and such, not needed
NULL,
NULL,
NULL);
ShowWindow(windowHandle, SW_RESTORE);
Whats left to do is now taking the screenshot of the screen and drawing it on form. Which i fail at this part.
When i googled, i first saw SetPixel function but to draw the form it took like half a minute. it was veerry slow. Then people said use Device Context (its the forms' drawing data in the memory as i understood) and draw on that, it will be much quicker then just update the window. And here's what i did;
int nScreenWidth = GetSystemMetrics(SM_CXSCREEN);
int nScreenHeight = GetSystemMetrics(SM_CYSCREEN);
HDC hdc = GetDC(windowHandle);
BitBlt(hdc, 0, 0, nScreenWidth, nScreenHeight, GetDC(NULL), 0, 0, SRCCOPY | CAPTUREBLT);
UpdateWindow(windowHandle);
ShowWindow(windowHandle, SW_RESTORE);
UpdateWindow(windowHandle);
As you can guess, it didn't work. My form is blank white. I don't understand if i should write this on WM_PAINT message on WindProc or not. I tried many variations to this and actually one point it worked i guess but then stopped working when i changed something and i couldn't get it to work again...
thank you.
Thanks for the comments, i did some more research on WM_PAINT message. And i found this golden document:
http://www.winprog.org/tutorial/bitmaps.html
My code in the original post stays the same, i only added 2 things;
1-Taking a screenshot of the screen and saving it;
(got it from here:
How can I take a screenshot in a windows application?)
// get the device context of the screen
HDC hScreenDC = GetDC(NULL);
// and a device context to put it in
HDC hMemoryDC = CreateCompatibleDC(hScreenDC);
int width = GetSystemMetrics(SM_CXSCREEN);
int height = GetSystemMetrics(SM_CYSCREEN);
// hBitmap is a HBITMAP that i declared globally to use within WM_PAINT
// maybe worth checking these are positive values
hBitmap = CreateCompatibleBitmap(hScreenDC, width, height);
// get a new bitmap
HBITMAP hOldBitmap = (HBITMAP) SelectObject(hMemoryDC, hBitmap);
BitBlt(hMemoryDC, 0, 0, width, height, hScreenDC, 0, 0, SRCCOPY);
hBitmap = (HBITMAP) SelectObject(hMemoryDC, hOldBitmap);
// clean up
DeleteDC(hMemoryDC);
ReleaseDC(NULL,hScreenDC);
// now your image is held in hBitmap. You can save it or do whatever with it
2- Painting via WM_PAINT:
switch (message)
{
case WM_PAINT:{
int nScreenWidth = GetSystemMetrics(SM_CXSCREEN);
int nScreenHeight = GetSystemMetrics(SM_CYSCREEN);
BITMAP bm;
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
HDC hdcMem = CreateCompatibleDC(hdc);
HBITMAP hbmOld = (HBITMAP) SelectObject(hdcMem, hBitmap);
GetObject(hBitmap, sizeof(bm), &bm);
BitBlt(hdc, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);
SelectObject(hdcMem, hbmOld);
DeleteDC(hdcMem);
EndPaint(hwnd, &ps);
}
return 0;
Note that i'm totally new to GDI, window stuff. I just kinda mashed pieces together i found here and there, but it works xd
thanks all for the help.
EDIT: also, just a quick info. If your display settings have some kinda scaling, screenshots are also scaled. What this means is if you have per se 125% scaling, then the screenshot will not be the actual fullscreen. To prevent this you need to have a manifest file.
https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests
the setting we are looking for is DPI awareness. here's my manifest file;
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
my .rc file:
#include "winuser.h"
1 RT_MANIFEST scanner.exe.manifest

How to adjust window border size

I have a window, which I want it to behave like a toggle button. Once clicked it will add 4px border and clicking after will make the border disappear. I figured how to make the window behave like a toggle button using BS_PUSHLIKE and Button_SetCheck() but can't seem to figure out how to adjust the border size for this window.
Thanks to all who take their time to help
Maybe you can use MoveWindow to resize the window, and then draw the border yourself, like this,
Draw a borderless window first:
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
100, 100, 800, 600, nullptr, nullptr, hInstance, nullptr);
LONG lStyle = GetWindowLong(hWnd, GWL_STYLE);
lStyle &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU);
SetWindowLong(hWnd, GWL_STYLE, lStyle);
Then handle the window border in the WM_LBUTTONDOWN message:
int num = 0;
case WM_LBUTTONDOWN:
{
RECT rcWind;
HDC dc = GetDC(hWnd);
GetWindowRect(hWnd, &rcWind);
if (num >= 0)
{
num--;
RECT rcClient;
MoveWindow(hWnd, rcWind.left - 4, rcWind.top - 4, 8 + rcWind.right - rcWind.left, 8 + rcWind.bottom - rcWind.top, TRUE);
GetClientRect(hWnd, &rcClient);
HPEN hPen = CreatePen(PS_SOLID, 4, RGB(255, 128, 1));
HGDIOBJ hOldPen = SelectObject(dc, hPen);
Rectangle(dc, rcClient.left, rcClient.top, rcClient.right, rcClient.bottom);
DeleteObject(hPen);
}
else if (num < 0)
{
MoveWindow(hWnd, rcWind.left + 4, rcWind.top + 4, rcWind.right - rcWind.left - 8, rcWind.bottom - rcWind.top - 8, TRUE);
num++;
}
}
break;

BitBlt not updating WM_PAINT in Windows C++ API

I have the following function to draw a loaded bitmap to a window.
void OnPaint(HWND hwnd) {
PAINTSTRUCT ps;
HDC hdc;
BITMAP bitmap;
HDC hdcMem;
HGDIOBJ oldBitmap;
hdc = BeginPaint(hwnd, &ps);
hdcMem = CreateCompatibleDC(hdc);
HBITMAP bmp = mainBitmap;
oldBitmap = SelectObject(hdcMem, mainBitmap);
GetObject(bmp, sizeof(bitmap), &bitmap);
x += 64;
RECT rect;
rect.left = x;
rect.top = 0;
rect.right = x+64;
rect.bottom = 64;
HBITMAP hBmp = CreateCompatibleBitmap(
hdc, // Handle to a device context
rect.right - rect.left, // Bitmap width
rect.bottom - rect.top // Bitmap height
);
BitBlt(
hdc, // Destination rectangle context handle
0, // Destination rectangle x-coordinate
0, // Destination rectangle y-coordinate
rect.right - rect.left, // Destination rectangle width
rect.bottom - rect.top, // Destination rectangle height
hdcMem, // A handle to the source device context
rect.left, // Source rectangle x-coordinate
rect.top, // Source rectangle y-coordinate
SRCCOPY // Raster-operation code
);
SelectObject(hdcMem, oldBitmap);
DeleteDC(hdcMem);
EndPaint(hwnd, &ps);
}
And I have the following image loaded into HBITMAP mainBitmap:
The image is drawn when the window opens successfully, and I see the first icon in the sprite bitmap (yellow grapple hook), but my issue is, when I press 'C' to re-paint the window, the image does not change to the next icon in the sprite image.
Things I Know
On initialization, x = 64;
Every time I press 'C', paint is called. (Confirmed in Visual
Studio Debugger)
x is incremented by 64 each time OnPaint is called.
Why is the graphic not changing?
Here is my WindowsProc function to handle the WM_PAINT message:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
HANDLE_MSG(hwnd, WM_PAINT, OnPaint);
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
Try to call function InvalidateRect() to update region.

After createwindow(...), how to give the window a color?

I have created a window whose handle is handle_parent. Then I created a child window as following:
hwnd_child = CreateWindow(child_class_name, _T(""),
WS_CHILDWINDOW, 0, 0, 0, 0, hwnd_parent, (HMENU)0, ghinst, NULL);
ShowWindow(win->hwndSplitterBar, SW_SHOW);
UpdateWindow(win->hwndSplitterBar);
I would like to set the color of child window "child". If I do nothing, the color is grey by default.
How could I set its color? I would like to keep the color as black permanent, do change in anycase.
Create a brush of the desired color and then pass it in the hbrBackground member of the WNDCLASS struct when calling RegisterClass to register your window class.
The system will delete this brush automatically when you call UnregisterClass so once you have passed this brush to RegisterClass you can forget all about it and must not attempt to delete it yourself.
This example may be helpful:
//Setting the background color of a window during window class registration
WNDCLASS wc = { 0 } ( or WNDCLASS wc; memset(&wc, 0, sizeof(wc)); )
...
...
...
wc.hbrBackground = CreateSolidBrush(0x000000ff); // a red window class background
...
...
RegisterClass(&wc);
// Setting the background during WM_ERASEBKGND
LRESULT CALLBACK YourWndProc(HWND hwnd, UINT umsg, WPARAM,LPARAM)
{
switch( umsg )
{
case WM_ERASEBKGND:
{
RECT rc;
GetClientRect(hwnd, &rc);
SetBkColor((HDC)wParam, 0x000000ff); // red
ExtTextOut((HDC)wParam, 0, 0, ETO_OPAQUE, &rc, 0, 0, 0);
return 1;
}
// or in WM_PAINT
case WM_PAINT:
{
PAINTSTRUCT ps;
RECT rc;
HDC hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rc);
SetBkColor(hdc, 0x000000ff); // red
ExtTextOut(hdc, 0, 0, ETO_OPAQUE, &rc, 0, 0, 0);
EndPaint(hwnd, &ps);
break;
}
...
...
...
default:
return DefWindowProc(...);
}
return 0;
}
Use CreateSolidBrush()::
WNDCLASS wc = { 0 } ( or WNDCLASS wc; memset(&wc, 0, sizeof(wc)); )
...
wc.hbrBackground = CreateSolidBrush(RGB(255,0,0)) or CreateSolidBrush(0x000000ff); // a red window class background

How to set pixel on mouse click in C++ using WinApi (GDI) in GUI window?

I'm trying to set pixel by mouse click, but nothing happens when I click. Here is part of my code.
First, I control window size changing in WM_SIZE.
Than, at first time when I want to set pixel by mouse I get window's width and height, then copy window's content to memory HDC and HBITMAP (in Store Window) (HBITMAP size equal to (width,height)). In fact, I copy to memory only clear window.
And than in any case I set pixel to memory DC. In next WM_PAINT message handling I'm drawing memory DC to screen.
.....
case WM_SIZE:
{
CheckWidthHeight();
break;
}
case WM_MBUTTONDOWN:
{
if (firstTimeDraw)
{
CheckWidthHeight();
StoreWindow();
firstTimeDraw = false;
}
SetPixel(memoryDC, LOWORD(lParam), HIWORD(lParam), RGB(0,0,0));
break;
}
case WM_PAINT:
{
RestoreWindow();
break;
}
.....
where my functions and variables is:
HDC memoryDC;
HBITMAP memoryBitmap;
int width = 0, height = 0;
bool firstTimeDraw = true;
void CheckWidthHeight()
{
RECT clientRect;
GetClientRect(hwnd, &clientRect);
width = clientRect.right - clientRect.left;
height = clientRect.bottom - clientRect.top;
}
//Copy real window content to memory window
void StoreWindow()
{
HDC hDC = GetDC(hwnd);
memoryDC = CreateCompatibleDC(hDC);
memoryBitmap = CreateCompatibleBitmap(hDC, width, height);
SelectObject(memoryDC, memoryBitmap);
BitBlt(memoryDC, 0, 0, width, height, hDC, 0, 0, SRCCOPY);
ReleaseDC(hwnd, hDC);
}
//Copy memory windows content to real window at the screen
void RestoreWindow()
{
PAINTSTRUCT ps;
HDC hDC = BeginPaint(hwnd, &ps);
memoryDC = CreateCompatibleDC(hDC);
SelectObject(memoryDC, memoryBitmap);
BitBlt(hDC, 0, 0, width, height, memoryDC, 0, 0, SRCCOPY);
EndPaint(hwnd, &ps);
}
What I'm doing wrong?
UPD:
A shot in the dark: You're handling the middle button click. Are you by any chance clicking on the left or right mouse buttons? :)
Ok. Now I use WM_LBUTTONUP or WM_LBUTTONDOWN. Nothing happens again.
UPD2:
When you change the memory DC, you'll also want to invalidate the part of the window that is affected so that Windows will generate a WM_PAINT message for it. InvalidateRect would be a good place to start.
I placed this code
RECT rect;
GetClientRect(hwnd, &rect);
InvalidateRect(hwnd, &rect, true);
before EndPaint. Nothing. Than I move it after EndPaint. Nothing.
In the WM_PAINT handler, you need to use a DC provided by BeginPaint and call EndPaint when you're done with it.
I do it in RestoreWindow().
I don't know yet what's the problem...
UPD3:
InvalidateRect() needs to happen in the WM_?BUTTONDOWN handler after the SetPixel (not in RestoreWindow())- it's what tells windows that you want to get a WM_PAINT in the first place.
Ok. I've done it before you wrote this message. Still don't work.
UPD4:
Thank you a lot, Remy! Thank you to all the rest. Now all right!!
Two things.
When you change the memory DC, you'll also want to invalidate the part of the window that is affected so that Windows will generate a WM_PAINT message for it. InvalidateRect would be a good place to start.
In the WM_PAINT handler, you need to use a DC provided by BeginPaint and call EndPaint when you're done with it.
When you call RestoreWindow() to draw the bitmap onscreen, you are wiping out your memoryDC variable that you used to draw the pixels with. The bitmap is still selected into the original HDC that you have now lost, and a bitmap cannot be selected into multiple HDCs at the same time (the MSDN documentation for SelectObject() says as much). So you are not actually drawing the bitmap onscreen at all.
There is no need to call CreateCompatibleDC() or SelectObject() inside of RestoreWindow() because you already have the bitmap and memory HDC set up inside of StoreWindow(), so they use them as-is instead.
Try this:
HDC memoryDC = NULL;
HBITMAP memoryBitmap = NULL;
int width = 0, height = 0;
void CheckWidthHeight()
{
RECT clientRect;
GetClientRect(hwnd, &clientRect);
width = clientRect.right - clientRect.left;
height = clientRect.bottom - clientRect.top;
}
void StoreWindow()
{
HDC hDC = GetDC(hwnd);
memoryDC = CreateCompatibleDC(hDC);
memoryBitmap = CreateCompatibleBitmap(hDC, width, height);
SelectObject(memoryDC, memoryBitmap);
BitBlt(memoryDC, 0, 0, width, height, hDC, 0, 0, SRCCOPY);
ReleaseDC(hwnd, hDC);
}
void RestoreWindow()
{
PAINTSTRUCT ps;
HDC hDC = BeginPaint(hwnd, &ps);
if (memoryDC)
BitBlt(hDC, 0, 0, width, height, memoryDC, 0, 0, SRCCOPY);
EndPaint(hwnd, &ps);
}
...
case WM_SIZE:
{
CheckWidthHeight();
break;
}
case WM_LBUTTONDOWN:
{
if (!memoryDC)
StoreWindow();
if (memoryDC)
{
SetPixel(memoryDC, LOWORD(lParam), HIWORD(lParam), RGB(0,0,0));
RECT rect;
rect.left = LOWORD(lParam);
rect.top = HIWORD(lParam);
rect.right = rect.left + 1;
rect.bottom = rect.top + 1;
InvalidateRect(hwnd, &rect, TRUE);
}
break;
}
case WM_PAINT:
{
RestoreWindow();
break;
}
...