How to display animated GIF in Static Control? - c++

I'm trying to display an animated GIF, I managed to make it work. However, I have an issue where the other painted images and text are affecting by the timer, they're being repaint. I tried solving it by using FillRect() but they're flickering.
So I tried to use this Static Control where you can subclass and paint on it.
WNDPROC StaticWndProc = NULL;
GDIHelper gdiHelper;
LRESULT CALLBACK MyStaticWndProc2(HWND hwnd, UINT Message, WPARAM wparam, LPARAM lparam) { //Call back for static control.
switch(Message) {
case WM_TIMER:{
gdiHelper.OnTimer(); // Do something on timer.
return 0;
}
case WM_PAINT:{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
gdiHelper.DrawItem(hdc, 15, 15, 95, 95); //draw the GIF image.
EndPaint(hwnd, &ps);
return TRUE;
}
case WM_DESTROY:{
gdiHelper.Desrtroy();
return 0;
}
}
return CallWindowProc(StaticWndProc, hwnd, Message, wparam, lparam); //v2
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch(message) {
case WM_CREATE: {
HWND staticcontrol = CreateWindowEx(0, L"STATIC", NULL, WS_CHILD | WS_VISIBLE, 150, 200, 124, 124, hWnd, NULL, NULL, NULL); //create the static control.
StaticWndProc = (WNDPROC)SetWindowLongPtr(staticcontrol, GWLP_WNDPROC, (LPARAM)MyStaticWndProc2); //subclass the static control.
gdiHelper.LoadImageFromFile(hWnd, ID_OF_YOUR_TIMER, "C:\\spinner.gif", L"GIF"); //load the GIF.
break;
}
case WM_PAINT:{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hWnd, &ps);
//paint other images and text here...
EndPaint(hWnd, &ps);
break;
}
case WM_DESTROY:{
PostQuitMessage(0);
break;
}
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
Here are the functions of GDIHelper.cpp class responsible for animating the GIF.
#include "GDIHelper.h"
/**
GDIHelper.h has the following declarations;
public:
GDIHelper();
void LoadImageFromResource(HWND hWnd, UINT_PTR timer_id, HMODULE hMod, const wchar_t* resid, const wchar_t* restype);
void LoadImageFromFile(HWND hWnd, UINT_PTR timer_id, string file_location, const wchar_t* restype);
void OnTimer();
void Desrtroy();
void Stop(UINT_PTR timer_id);
void DrawItem(HDC hdc, int xPosition, int yPosition, int width, int height);
private:
void InitializeImage();
bool IsFileExist(string file_name);
void AnimateGIF();
HWND hwnd;
Image* m_pImage;
GUID* m_pDimensionIDs;
UINT m_FrameCount;
PropertyItem* m_pItem;
UINT m_iCurrentFrame;
UINT_PTR timer_id;
BOOL m_bIsPlaying;
BOOL isPlayable;
**/
/** GDIHelper is a class helper to display images and animated GIF **/
GDIHelper::GDIHelper() {
timer_id = 0;
m_FrameCount = 0;
m_iCurrentFrame = 0;
m_pImage = NULL;
m_pDimensionIDs = NULL;
m_pItem = NULL;
hwnd = NULL;
m_bIsPlaying = FALSE;
isPlayable = FALSE;
}
/** Function to destroy objects and arrays, call this function on WM_DESTROY of WinProc. **/
void GDIHelper::Desrtroy() {
if(m_pDimensionIDs) {
delete[] m_pDimensionIDs;
}
if(m_pItem) {
free(m_pItem);
}
if(m_pImage) {
delete m_pImage;
}
}
/** Functon to load the next frame of GIF, must be call on WM_TIMER. **/
void GDIHelper::OnTimer() {
if(isPlayable) {
KillTimer(hwnd, timer_id);
GUID Guid = FrameDimensionTime;
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
SetTimer(hwnd, 120, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, NULL);
m_iCurrentFrame = (++m_iCurrentFrame) % m_FrameCount;
InvalidateRect(hwnd, NULL, FALSE);
}
}
/** Private function, call this to animate the GIF image, should be call before drawing the image usually on WM_PAINT. **/
void GDIHelper::AnimateGIF() {
if(m_bIsPlaying == TRUE) {
return;
}
if(isPlayable) {
m_iCurrentFrame = 0;
GUID Guid = FrameDimensionTime;
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
SetTimer(hwnd, 120, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, NULL);
++m_iCurrentFrame;
InvalidateRect(hwnd, NULL, FALSE);
m_bIsPlaying = TRUE;
}
}
/** Function to draw the image in Window, must be call on WM_PAINT. **/
void GDIHelper::DrawItem(HDC hdc, int xPosition, int yPosition, int width, int height) {
AnimateGIF(); //This will only works if the image has more than one frame.
Graphics g(hdc);
g.DrawImage(m_pImage, xPosition, yPosition, width, height);
}
/** Private function, accessible only in this class, check if file exist. **/
bool GDIHelper::IsFileExist(string file_name) {
struct stat buffer;
return (stat(file_name.c_str(), &buffer) == 0);
}
/** Private function, function to count and get the frame of image. **/
void GDIHelper::InitializeImage() {
UINT count = m_pImage->GetFrameDimensionsCount();
m_pDimensionIDs = new GUID[count];
m_pImage->GetFrameDimensionsList(m_pDimensionIDs, count);
m_FrameCount = m_pImage->GetFrameCount(&m_pDimensionIDs[0]);
if(m_FrameCount > 1) { //frame of GIF is more than one, all good, we don't want the error of `Access violation reading location`
isPlayable = TRUE;
OutputDebugString(_T("NOTICED: GDIHelper::InitializeImage >> Image file has more than 1 frame, its playable.\n"));
}
UINT TotalBuffer = m_pImage->GetPropertyItemSize(PropertyTagFrameDelay);
m_pItem = (PropertyItem*)malloc(TotalBuffer);
m_pImage->GetPropertyItem(PropertyTagFrameDelay, TotalBuffer, m_pItem);
}
/** Function to Load Image from Local File. **/
void GDIHelper::LoadImageFromFile(HWND hWnd, UINT_PTR ttimer_id, string file_name, const wchar_t* restype) {
hwnd = hWnd;
timer_id = ttimer_id;
if(!IsFileExist(file_name)) {
OutputDebugString(_T("ERROR: GDIHelper::LoadImageFromFile >> Invalid file or not exist\n"));
return;
}
std::wstring widestr = std::wstring(file_name.begin(), file_name.end()); // Convert the string file_name to wstring.
m_pImage = Image::FromFile(widestr.c_str()); //Convert the wtring to wchar and initialize.
InitializeImage(); //Initialize the image.
}
Only the single frame of GIF is drawn, it looks like the timer is not working (I'm not sure), because it's not animating.

You are passing the wrong handle to a window. In your WndProc callback, instead of hWnd you need to pass the staticcontrol you created.
It should be;
case WM_CREATE: {
HWND staticcontrol = CreateWindowEx(0, L"STATIC", NULL, WS_CHILD | WS_VISIBLE, 150, 200, 124, 124, hWnd, NULL, NULL, NULL); //create the static control.
StaticWndProc = (WNDPROC)SetWindowLongPtr(staticcontrol, GWLP_WNDPROC, (LPARAM)MyStaticWndProc2); //subclass the static control.
/** Instead of `hWnd`, the handle of the parent Window, you need to use the handle window you created for static control which is `staticcontrol` **/
gdiHelper.LoadImageFromFile(staticcontrol, ID_OF_YOUR_TIMER, "C:\\spinner.gif", L"GIF"); //load the GIF.
break;
}
In addition to the WM_DESTROY on your WndProc callback, you may want to do this;
case WM_DESTROY:{
SetWindowLong(staticcontrol, GWL_WNDPROC, (LPARAM)MyStaticWndProc2);
PostQuitMessage(0);
break;
}

Related

How to paint a text control on mouse hover?

I am having troubles painting my static text controls. I have done some reading on this matter, and I have also tried some codes, but none of them worked. The closest that I managed to get was until the text control detects my mouse hover event. For this, I use control subclassing. However, I don't know what I can do to paint the text controls upon mouse hover.
My questions are:
I am confused whether to place the code responsible for painting the text control either in WM_MOUSEMOVE or WM_MOUSEHOVER in the subclass. The reason is because in the WM_MOUSEMOVE, I set it so that when the rectangle area of the text control detects the cursor, painting should happen. At the same time, I feel weird because shouldn't that particular role already be done by WM_MOUSEHOVER?
I really need help on how I could highlight the text control when the mouse is hovered.
// global text control handler
HWND txtHwnd; //somewhere in the global area of codes
//class for mouse event tracking
class MouseTrackEvents
{
bool m_bMouseTracking;
public:
MouseTrackEvents() : m_bMouseTracking(false) {}
void OnMouseMove(HWND hwnd)
{
if (!m_bMouseTracking)
{
// Enable mouse tracking.
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.hwndTrack = hwnd;
tme.dwFlags = TME_HOVER;
tme.dwHoverTime = 100; //0.1s
TrackMouseEvent(&tme);
m_bMouseTracking = true;
}
}
void Reset(HWND hwnd)
{
m_bMouseTracking = false;
}
};
//subclass proc for text control
LRESULT CALLBACK OwnerTxtProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) {
//local mouse track object
MouseTrackEvents mouseTrack;
const int numOfMsg = 4;
int myMsgBox[numOfMsg]; //limit to only 4
HBRUSH hbrBkgndA = CreateSolidBrush(RGB(0, 255, 0));
//just want to print what wparam is..
int wmId, wmEvent;
wchar_t wParamHI[1000];
wchar_t wParamLO[1000];
//for painting
COLORREF hbrHighlight;
//HBRUSH hbrHighlight;
HDC txtHDC;
RECT txtRect;
POINT txtPt;
RECT txtRt;
switch (uMsg) {
case WM_CREATE: {
//testing purposes
myMsgBox[0] = MessageBox(nullptr, L"Yessir!", L":D", MB_OK | MB_ICONINFORMATION);
break;
}
case WM_MOUSEMOVE: {
mouseTrack.OnMouseMove(hWnd); //activate mouse tracking
GetWindowRect(hWnd, &txtRt); //see what value lies within
GetCursorPos(&txtPt);
if (PtInRect(&txtRt, txtPt)) {
//to check whether cursor is in text rect or not
MessageBox(nullptr, L"Okea..nice..!", L":D", MB_OK | MB_ICONINFORMATION);
//i dont know what to do here
//i was thinking that painting should be placed here...
//HBRUSH hbrBkgndEdit = CreateSolidBrush(RGB(100, 100, 255));
}
else {
//to check whether cursor is in text rect or not
MessageBox(nullptr, L"Oh noooo!", L":D", MB_OK | MB_ICONINFORMATION);
}
break;
}
case WM_MOUSEHOVER: {
//do something when txt ctrl is hovered
//to test whether wm_mousehover works, and the text control is mouse hovered
//myMsgBox[1] = MessageBox(nullptr, L"Yahooooo!", L":D", MB_OK | MB_ICONINFORMATION);
OutputDebugString(L"--------------This text control is being hovered---------------\n");
//testing painting when hovered
//plan is to send message to hChild to manage the painting since it is the parent window
//HDC theHDC = (HDC)wParam;
//SendMessage(hWnd, WM_CTLCOLORSTATIC, (WPARAM)theHDC, (LPARAM)txtHwnd);
//SendMessage(hWnd, WM_CTLCOLORSTATIC, (WPARAM)theHDC, lParam);
//for painting
//hbrHighlight = (COLORREF)(GetSysColor(COLOR_HIGHLIGHT));
//HDC hdcStatic = (HDC)wParam;
////SetTextColor(hdcStatic, RGB(0, 0, 0));
//SetBkColor(hdcStatic, hbrHighlight);
//for painting II
//HDC txtCtrlMe = (HDC)wParam;
//SetTextColor(txtCtrl, RGB(0, 0, 0));
//SetBkColor(txtCtrlMe, RGB(0, 255, 0));
//SendMessage(hChild, WM_CTLCOLORSTATIC, (WPARAM)txtCtrlMe, (LPARAM)txtHwnd);
//DeleteBrush(hbrBkgndA);
mouseTrack.Reset(hWnd);
//return (INT_PTR)hbrHighlight;
//mouseTrack.Reset(hWnd);
break;
//return TRUE;
}
//case WM_CTLCOLORSTATIC:
//{
// HDC txtCtrlMe = (HDC)wParam;
// //SetTextColor(txtCtrl, RGB(0, 0, 0));
// SetBkColor(txtCtrlMe, RGB(0, 255, 0));
// if (hbrBkgndA == NULL)
// {
// hbrBkgndA = CreateSolidBrush(RGB(255, 255, 255)); //eqivalent to delete brush objet
// }
// return (INT_PTR)hbrBkgndA;
// //break;
//}
//painting
/*
SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
SendMessage(hChild, WM_CTLCOLORSTATIC, (WPARAM)txtCtrlMe, (LPARAM)txtHwnd);
*/
//you can just ignore this, this is just for me to see whats inside wParam of the subclass
case WM_LBUTTONDOWN: {
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
int HI = swprintf_s(wParamHI, 1000, L"wParamHI: %d", wmEvent);
MessageBox(nullptr, wParamHI, L":D", MB_OK | MB_ICONINFORMATION);
int LO = swprintf_s(wParamLO, 1000, L"wParamLO: %d", wmId);
MessageBox(nullptr, wParamLO, L":D", MB_OK | MB_ICONINFORMATION);
break;
}
}
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
//child window proc, which contains the static text control
LRESULT CALLBACK WndProcChild(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
//...some other codes...
txtHwnd = CreateWindow(L"Static", L"Hello World!", WS_VISIBLE | WS_CHILD | SS_NOTIFY, 100, 100, 120, 15, hChild, (HMENU)testingTxtID, nullptr, nullptr);
//control subclassing
//a.k.a attaching subclass proc to the static text control
SetWindowSubclass(txtHwnd, OwnerTxtProc, 0, 0);
//...some other codes...
default: {
return DefWindowProc(hChild, message, wParam, lParam);
}
return 0;
}
Please ignore the codes I commented out in WM_MOUSEHOVER and also WM_CTLCOLORSTATIC inside OwnerTxtProc(...). They are just codes that I used to try to paint the text control, which didn't work.
All painting operations for your control should be only inside of a WM_CTLCOLORSTATIC handler, or even a WM_PAINT handler.
To trigger a repaint of the control, simply invalidate the control's client area using InvalidateRect().
If you want to change the painting conditions, store the relevant information in variables that the painting code uses, and then update those variables before invalidating the control.
Try something like this:
HWND txtHwnd;
bool bMouseTracking = false;
COLORREF bkgndColor = ...; // whatever you want...
COLORREF txtColor = ...; // whatever you want...
HBRUSH hbrBkgnd = nullptr;
const UINT APP_CTLCOLORSTATIC = WM_APP + 1;
LRESULT CALLBACK StaticTxtProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) {
switch (uMsg) {
case WM_NCDESTROY: {
RemoveWindowSubclass(hWnd, StaticTxtProc, uIdSubclass);
if (hbrBkgnd) {
DeleteBrush(hbrBkgnd);
hbrBkgnd = NULL;
}
bMouseTracking = false;
break;
}
case WM_MOUSEMOVE: {
if (!bMouseTracking) {
TRACKMOUSEEVENT tme = {};
tme.cbSize = sizeof(tme);
tme.hwndTrack = hWnd;
tme.dwFlags = TME_HOVER;
tme.dwHoverTime = 100; //0.1s
bMouseTracking = TrackMouseEvent(&tme);
}
RECT txtRt;
GetWindowRect(hWnd, &txtRt);
POINT txtPt;
GetCursorPos(&txtPt);
if (PtInRect(&txtRt, txtPt)) {
bkgndColor = ...; // whatever you want...
txtColor = ...; // whatever you want...
InvalidateRect(hWnd, NULL, TRUE);
}
break;
}
case WM_MOUSEHOVER: {
mMouseTracking = false;
bkgndColor = ...; // whatever you want...
txtColor = ...; // whatever you want...
InvalidateRect(hWnd, NULL, TRUE);
break;
}
case APP_CTLCOLORSTATIC: {
HDC hdc = reinterpret_cast<HDC>(wParam);
SetTextColor(hdc, txtColor);
SetBkColor(hdc, bkgndColor);
if (hbrBkgnd)
DeleteBrush(hbrBkgnd);
hbrBkgnd = CreateSolidBrush(bkgndColor);
return reinterpret_cast<LRESULT>(bkgndColor);
}
case WM_LBUTTONDOWN: {
// ...
break;
}
}
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
LRESULT CALLBACK WndProcChild(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_CREATE: {
bkgndColor = ...; // whatever you want...
txtColor = ...; // whatever you want...
txtHwnd = CreateWindow(L"Static", L"Hello World!", WS_VISIBLE | WS_CHILD | SS_NOTIFY, 100, 100, 120, 15, hChild, (HMENU)testingTxtID, nullptr, nullptr);
SetWindowSubclass(txtHwnd, StaticTxtProc, 0, 0);
break;
}
case WM_CTLCOLORSTATIC: {
return SendMessage(reinterpret_cast<HWND>(lParam), APP_CTLCOLORSTATIC, wParam, 0);
}
//...some other codes...
default: {
return DefWindowProc(hChild, message, wParam, lParam);
}
}
return 0;
}
(Posting solution on behalf of the question author to move it to the answer space).
I managed to deal with my problem on this matter.
//global variable(s)
COLORREF bkgndColor;
COLORREF txtColor;
HBRUSH hbrBkgnd = nullptr;
const UINT APP_CTLCOLORSTATIC = WM_APP + 1;
//class for mouse event
class MouseTrackEvents
{
bool m_bMouseTracking;
public:
MouseTrackEvents() : m_bMouseTracking(false) {}
void OnMouseMove(HWND hwnd)
{
if (!m_bMouseTracking)
{
// Enable mouse tracking.
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.hwndTrack = hwnd;
tme.dwFlags = TME_HOVER | TME_LEAVE;
tme.dwHoverTime = 10; //1ms
TrackMouseEvent(&tme);
m_bMouseTracking = true;
}
}
void Reset(HWND hwnd)
{
m_bMouseTracking = false;
}
};
//proc for static text contorl subclass
LRESULT CALLBACK OwnerTxtProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) {
//local mouse track variable
MouseTrackEvents mouseTrack;
RECT txtRt;
GetWindowRect(hWnd, &txtRt);
POINT txtPt;
GetCursorPos(&txtPt);
switch (uMsg) {
//...some other codes...
case WM_NCDESTROY: {
RemoveWindowSubclass(hWnd, OwnerTxtProc, uIdSubclass);
if (hbrBkgnd) {
DeleteBrush(hbrBkgnd);
hbrBkgnd = NULL;
}
mouseTrack.Reset(hWnd);
break;
}
case WM_MOUSEMOVE: {
mouseTrack.OnMouseMove(hWnd); //activate mouse tracking
break;
}
case WM_MOUSEHOVER: {
if (PtInRect(&txtRt, txtPt)) {
OutputDebugString(L"--------------This text control is being hovered---------------\n");
bkgndColor = RGB(0, 0, 200); // blue
txtColor = RGB(200, 0, 0); // red
InvalidateRect(hWnd, NULL, FALSE);
}
else {
OutputDebugString(L"--------------This text control is being left---------------\n");
bkgndColor = RGB(100, 0, 0); // red bg
txtColor = RGB(0, 100, 0); // green txt
InvalidateRect(hWnd, NULL, FALSE);
}
mouseTrack.Reset(hWnd);
break;
}
case WM_MOUSELEAVE: {
if (!(PtInRect(&txtRt, txtPt))) {
OutputDebugString(L"--------------This text control is being hovered---------------\n");
bkgndColor = RGB(100, 0, 0); // red bg
txtColor = RGB(0, 100, 0); // green txt
InvalidateRect(hWnd, NULL, FALSE);
}
mouseTrack.Reset(hWnd);
break;
}
case APP_CTLCOLORSTATIC: {
HDC hdc = reinterpret_cast<HDC>(wParam);
SetTextColor(hdc, txtColor);
SetBkColor(hdc, bkgndColor);
if (hbrBkgnd) {
DeleteBrush(hbrBkgnd);
}
hbrBkgnd = CreateSolidBrush(bkgndColor);
//return reinterpret_cast<LRESULT&>(bkgndColor); //--> error --> so i added '&'
return (LRESULT)hbrBkgnd;
}
//...some other codes...
}
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
//parent of static text control
LRESULT CALLBACK WndProcChild(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
//...some other codes...
switch (message)
{
case WM_CTLCOLORSTATIC:
{
return SendMessage(reinterpret_cast<HWND>(lParam), APP_CTLCOLORSTATIC, wParam, 0);
}
case WM_CREATE: //put all sorts of stuff when the context menu is called
{
//font to be set to
hFont = CreateFont(17, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, TEXT("Segoe UI"));
//set font of the textCtrl
//doesnt work if handler target = hChild
for (int i = 0; i < textCtrlSize; i++) {
SendMessage(textCtrl[i], WM_SETFONT, (WPARAM)hFont, TRUE);
}
bkgndColor = RGB(100, 0, 0); // red bg
txtColor = RGB(0, 100, 0); // green txt
txtHwnd = CreateWindow(L"Static", L"Hello World!", WS_VISIBLE | WS_CHILD | SS_NOTIFY, 100, 100, 120, 30, hChild, (HMENU)testingTxtID, nullptr, nullptr);
SetWindowSubclass(txtHwnd, OwnerTxtProc, 0, 0);
break;
}
//...some other codes...
default:
return DefWindowProc(hChild, message, wParam, lParam);
}
return 0;
}

How to prevent Timer from freezing when titlebar is clicked or dragging?

I have a timer that is responsible for displaying the GIF image frame by frame. I noticed that when I right-clicked and hold the titilebar the timer pause for I think 1 second and when I left-clicked and hold the titlebar`, the timer pause until I released the mouse.
LRESULT CALLBACK GDIHelper::StaticControlProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) {
switch(uMsg) {
case WM_TIMER:
{
OnTimer(); // Do something on timer.
InvalidateRect(staticControl, NULL, FALSE);
return 0;
}
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
Graphics g(hdc);
g.DrawImage(m_pImage, 0, 0, width, height);
EndPaint(hwnd, &ps);
return TRUE;
}
case WM_DESTROY:
{
return 0;
}
}
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch(message) {
case WM_CREATE: {
staticControl = CreateWindowEx(0, L"STATIC", NULL, WS_CHILD | WS_VISIBLE | SS_OWNERDRAW, xPosition, yPosition, width, height, hWnd, NULL, NULL, NULL); //create the static control.
SetWindowSubclass(staticControl, &StaticControlProc, unique_id, 0);
gdiHelper.AnimateGIF();
break;
}
case WM_PAINT:
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hWnd, &ps);
//Paint other images and text here...
EndPaint(hWnd, &ps);
break;
}
case WM_DESTROY:
{
gdiHelper.Destroy();
PostQuitMessage(0);
break;
}
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
and here are the functions responsible for creating the timer.
void GDIHelper::OnTimer() {
if(isPlayable) {
GUID Guid = FrameDimensionTime;
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
m_iCurrentFrame = (++m_iCurrentFrame) % m_FrameCount;
}
}
void GDIHelper::AnimateGIF() {
if(m_bIsPlaying == TRUE) {
return;
}
m_iCurrentFrame = 0;
GUID Guid = FrameDimensionTime;
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
SetTimer(staticControl, 120, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, NULL);
++m_iCurrentFrame;
m_bIsPlaying = TRUE;
}
How to possibly prevent that?
The InvalidateRect() will not immediately redraw your control. It will simply schedule a future redraw for a specific rectangular area of the window. Making the animation freeze when you clicked or hold on the titlebar. Use InvalidateRect() followed by UpdateWindow(), this will forcefully perform immediate redrawing of the specified area of the window.
But it won't just solve the issue. Forget about your timer and use a non-blocking thread instead together with InvalidateRect() and UpdateWindow().
int animation_duration = 0;
void GDIHelper::TheAnimation() { //This function should be static.
if(m_bIsPlaying == TRUE) {
return;
}
m_iCurrentFrame = 0;
GUID Guid = FrameDimensionTime;
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
++m_iCurrentFrame;
m_bIsPlaying = TRUE;
animation_duration = ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10;
while(isPlayable) { //Make sure to set isPlayable to false on destroy to stop this thread.
std::this_thread::sleep_for(std::chrono::milliseconds(animation_duration)); //instead of timer, use sleep.
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
m_iCurrentFrame = (++m_iCurrentFrame) % m_FrameCount;
InvalidateRect(staticControl, NULL, FALSE);
UpdateWindow(staticControl); //perform immediate redrawing
}
}
LRESULT CALLBACK GDIHelper::StaticControlProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) {
switch(uMsg) {
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
Graphics g(hdc);
g.DrawImage(m_pImage, 0, 0, width, height); //draw your image.
EndPaint(hwnd, &ps);
return TRUE;
}
....
}
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}
Then just start the thread and use detach() for non-blocking UI.
std::thread t(TheAnimation);
t.detach(); // this will be non-blocking thread.
Read the comments in the code for some clarification and don't forget to set isPlayable to false on destroy to stop the thread.

How to display animated GIF?

I have an animated GIF, I'm trying to display it but I couldn't make it work. It won't run and throwing exception 0xC0000005: Access violation reading location 0x80000B00. at the line SetTimer(hWnd, ID_TIMER, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, (TIMERPROC)NULL);.
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
Image* m_pImage = new Image(L"spinner.gif");
UINT m_iCurrentFrame = 0;
UINT m_FrameCount = 0;
PropertyItem* m_pItem = 0;
switch(message) {
...
case WM_TIMER: {
//Because there will be a new delay value
KillTimer(hWnd, ID_TIMER);
//Change Active frame
GUID Guid = FrameDimensionTime;
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
//New timer
SetTimer(hWnd, ID_TIMER, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, NULL);
//Again move to the next
m_iCurrentFrame = (++m_iCurrentFrame) % m_FrameCount;
InvalidateRect(hWnd, NULL, FALSE);
break;
}
case WM_PAINT:{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hWnd, &ps);
Graphics graphics(hdc);
//First of all we should get the number of frame dimensions
//Images considered by GDI+ as:
//frames[animation_frame_index][how_many_animation];
UINT count = m_pImage->GetFrameDimensionsCount();
//Now we should get the identifiers for the frame dimensions
GUID* m_pDimensionIDs = new GUID[count];
m_pImage->GetFrameDimensionsList(m_pDimensionIDs, count);
//For gif image , we only care about animation set#0
WCHAR strGuid[40];
StringFromGUID2(m_pDimensionIDs[0], strGuid, 40);
m_FrameCount = m_pImage->GetFrameCount(&m_pDimensionIDs[0]);
//PropertyTagFrameDelay is a pre-defined identifier
//to present frame-delays by GDI+
UINT TotalBuffer = m_pImage->GetPropertyItemSize(PropertyTagFrameDelay);
m_pItem = (PropertyItem*)malloc(TotalBuffer);
m_pImage->GetPropertyItem(PropertyTagFrameDelay, TotalBuffer, m_pItem);
//Set Current Frame at #0
m_iCurrentFrame = 0;
GUID Guid = FrameDimensionTime;
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
//Use Timer
//NOTE HERE: frame-delay values should be multiply by 10
SetTimer(hWnd, ID_TIMER, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, (TIMERPROC)NULL);
//Move to the next frame
++m_iCurrentFrame;
InvalidateRect(hWnd, NULL, FALSE);
//Finally simply draw
graphics.DrawImage(m_pImage, 120, 120, 100, 100);
EndPaint(hWnd, &ps);
break;
}
...
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
Most of the example I found is WCF or the functions are depreciated.
Solution:
You can set m_pItem to global. In addition to this error, you have some other errors. For example, m_iCurrentFrame is always initialized to 0 in WM_PAINT.
And we only need to load the GIF image file once, and don't need to put it in WM_PAINT.
GIF loop play,
#include <memory>
#include <vector>
#include <algorithm>
#include <windows.h>
#include <objidl.h>
#include <GdiPlus.h>
#include <gdiplusimaging.h>
#include <tchar.h>
using namespace Gdiplus;
#pragma comment (lib,"Gdiplus.lib")
#define TIMER_ID 101
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
ULONG_PTR m_gdiplusToken;
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
Image gif(_T("spinner.gif"));
MSG msg = { 0 };
WNDCLASS wc = { 0 };
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.hbrBackground = NULL; // <= Do not provide a background brush.
wc.lpszClassName = L"anim_gif_player";
if (!RegisterClass(&wc))
return -1;
if (!CreateWindow(wc.lpszClassName,
L"Animated GIF player",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
0, 0, 1080, 800, 0, 0, hInstance, &gif))
return -2;
while (GetMessage(&msg, NULL, 0, 0) > 0)
DispatchMessage(&msg);
return 0;
}
std::vector<unsigned int> LoadGifFrameInfo(Image* image)
{
// I think animated gifs will always only have 1 frame dimension...
// the "dimension" being the frame count, but I could be wrong about this
int count = image->GetFrameDimensionsCount();
if (count != 1)
return std::vector<unsigned int>();
GUID guid;
if (image->GetFrameDimensionsList(&guid, 1) != 0)
return std::vector<unsigned int>();
int frame_count = image->GetFrameCount(&guid);
auto sz = image->GetPropertyItemSize(PropertyTagFrameDelay);
if (sz == 0)
return std::vector<unsigned int>();
// copy the frame delay property into the buffer backing an std::vector
// of bytes and then get a pointer to its value, which will be an array of
// unsigned ints
std::vector<unsigned char> buffer(sz);
PropertyItem* property_item = reinterpret_cast<PropertyItem*>(&buffer[0]);
image->GetPropertyItem(PropertyTagFrameDelay, sz, property_item);
unsigned int* frame_delay_array = (unsigned int*)property_item[0].value;
// copy the delay values into an std::vector while converting to milliseconds.
std::vector<unsigned int> frame_delays(frame_count);
std::transform(frame_delay_array, frame_delay_array + frame_count, frame_delays.begin(),
[](unsigned int n) {return n * 10; }
);
return frame_delays;
}
void GenerateFrame(Bitmap* bmp, Image* gif)
{
Graphics dest(bmp);
SolidBrush white(Color::White);
dest.FillRectangle(&white, 0, 0, bmp->GetWidth(), bmp->GetHeight());
if (gif)
dest.DrawImage(gif, 0, 0);
}
std::unique_ptr<Bitmap> CreateBackBuffer(HWND hWnd)
{
RECT r;
GetClientRect(hWnd, &r);
return std::make_unique<Bitmap>(r.right - r.left, r.bottom - r.top);
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static Image* animated_gif;
static std::unique_ptr<Bitmap> back_buffer;
static std::vector<unsigned int> frame_delays;
static int current_frame;
switch (message) {
case WM_CREATE: {
animated_gif = reinterpret_cast<Image*>(
reinterpret_cast<CREATESTRUCT*>(lParam)->lpCreateParams
);
if (!animated_gif || animated_gif->GetLastStatus() != 0) {
MessageBox(hWnd, _T("Unable to load animated gif"), _T("error"), MB_ICONERROR);
return 0;
}
// Create a bitmap the size of the window's clent area
back_buffer = CreateBackBuffer(hWnd);
// get the frame delays and thereby test that this is really an animated gif
frame_delays = LoadGifFrameInfo(animated_gif);
if (frame_delays.empty()) {
MessageBox(hWnd, _T("Invalid gif or not an animated gif"), _T("error"), MB_ICONERROR);
return 0;
}
current_frame = 0;
animated_gif->SelectActiveFrame(&FrameDimensionTime, current_frame);
GenerateFrame(back_buffer.get(), animated_gif);
SetTimer(hWnd, TIMER_ID, frame_delays[0], nullptr);
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
case WM_TIMER: {
KillTimer(hWnd, TIMER_ID);
current_frame = (current_frame + 1) % frame_delays.size();
animated_gif->SelectActiveFrame(&FrameDimensionTime, current_frame);
GenerateFrame(back_buffer.get(), animated_gif);
SetTimer(hWnd, TIMER_ID, frame_delays[current_frame], nullptr);
InvalidateRect(hWnd, nullptr, FALSE);
} break;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
Graphics g(hdc);
g.DrawImage(back_buffer.get(), 0, 0);
EndPaint(hWnd, &ps);
} break;
case WM_SIZE: {
back_buffer = CreateBackBuffer(hWnd);
GenerateFrame(back_buffer.get(), animated_gif);
} break;
case WM_CLOSE:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
As per as reading into the comment section, it looks like the error 0xC0000005: Access violation reading location 0x80000B00. at the line SetTimer(hWnd, ID_TIMER, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, (TIMERPROC)NULL); happened because the GIF contains only one frame.
You can avoid it by calling the timer only if there are more than one frame, for example;
if(m_FrameCount > 1){
//Continue calling the timer.
}
Here's another example;
/** These are global **/
Image* m_pImage;
GUID* m_pDimensionIDs;
UINT m_FrameCount;
PropertyItem* m_pItem;
UINT m_iCurrentFrame;
UINT_PTR timer_id = 123456; //set your own timer id
BOOL m_bIsPlaying;
BOOL isPlayable;
/** Functions **/
void LoadImage() {
m_pImage = Image::FromFile(L"C:\\Users\\username\\location_of_file\\spinner.gif");
UINT count = m_pImage->GetFrameDimensionsCount();
m_pDimensionIDs = new GUID[count];
m_pImage->GetFrameDimensionsList(m_pDimensionIDs, count);
WCHAR strGuid[39];
(void)StringFromGUID2(m_pDimensionIDs[0], strGuid, 39);
m_FrameCount = m_pImage->GetFrameCount(&m_pDimensionIDs[0]);
if(m_FrameCount > 1) { //frame of GIF is more than one, all good, we don't want the error of `Access violation reading location`
isPlayable = TRUE;
OutputDebugString(_T("Image file contains more than 1 frame, its playable.\n"));
}
UINT TotalBuffer = m_pImage->GetPropertyItemSize(PropertyTagFrameDelay);
m_pItem = (PropertyItem*)malloc(TotalBuffer);
m_pImage->GetPropertyItem(PropertyTagFrameDelay, TotalBuffer, m_pItem);
}
/** call this function on timer **/
void OnTimer() {
if(isPlayable) {
KillTimer(hwnd, timer_id);
GUID Guid = FrameDimensionTime;
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
SetTimer(hwnd, 120, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, NULL);
m_iCurrentFrame = (++m_iCurrentFrame) % m_FrameCount;
//Invalidate(FALSE);
InvalidateRect(hwnd, NULL, FALSE);
}
}
/** call this to animate the GIF image, should be call before drawing the image usually on WM_PAINT. **/
void AnimateGIF() {
if(m_bIsPlaying == TRUE) {
return;
}
if(isPlayable) {
m_iCurrentFrame = 0;
GUID Guid = FrameDimensionTime;
m_pImage->SelectActiveFrame(&Guid, m_iCurrentFrame);
SetTimer(hwnd, 120, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, NULL);
++m_iCurrentFrame;
InvalidateRect(hwnd, NULL, FALSE);
m_bIsPlaying = TRUE;
}
}
/** Function to draw the image in Window, must be call on WM_PAINT. **/
void DrawItem(HDC hdc, int xPosition, int yPosition, int width, int height) {
AnimateGIF(); //This will only works if the image has more than one frame. Otherwise, the single frame of image will be shown.
Graphics g(hdc);
g.DrawImage(m_pImage, xPosition, yPosition, width, height);
}
/** Function to destroy objects and arrays, call this function on WM_DESTROY of WinProc. **/
void Desrtroy() {
if(m_pDimensionIDs) {
delete[] m_pDimensionIDs;
}
if(m_pItem) {
free(m_pItem);
}
if(m_pImage) {
delete m_pImage;
}
}
Then on your WndProc, you can call the functions above;
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch(message) {
case WM_CREATE: {
LoadImage(); //1. Load the image
break;
}
case WM_TIMER: {
OnTimer(); //2. Call on timer.
break;
}
case WM_PAINT:
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hWnd, &ps);
DrawItem(hdc, 160, 150, 95, 95); //3. Draw your image, specify your own x and y Position, width and height.
EndPaint(hWnd, &ps);
break;
}
case WM_DESTROY:
{
Desrtroy(); //4. Don't forget to delete the images and free up the arrays.
PostQuitMessage(0);
break;
}
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
With this approach, you won't get the same error. The GIF will play forever if there are more than one frame. Otherwise, it will only display the single frame.
The project example can be found from this link, credit to SeynalKim.

How to make static window increase size on mouse hover

I have a bunch of windows inside another window.
Child Windows are created in the CreateBoard() method of MainWindow class. Create() method of MainWindow and GemWindow is from the MS Documentation BaseWindow abstract class and this is where the each class is registered with a call to RegisterClass()
template <class DERIVED_TYPE>
class BaseWindow
{
public:
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
DERIVED_TYPE* pThis = NULL;
if (uMsg == WM_NCCREATE)
{
CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam;
pThis = (DERIVED_TYPE*)pCreate->lpCreateParams;
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);
pThis->m_hwnd = hwnd;
}
else
{
pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
}
if (pThis)
{
//getting read access violation here why?
//because on resize you are not initializing tiles
//but resizing the thing
return pThis->HandleMessage(uMsg, wParam, lParam);
}
else
{
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
BaseWindow() : m_hwnd(NULL) { }
BOOL Create(
PCWSTR lpWindowName,
DWORD dwStyle,
DWORD dwExStyle = 0,
int x = CW_USEDEFAULT,
int y = CW_USEDEFAULT,
int nWidth = CW_USEDEFAULT,
int nHeight = CW_USEDEFAULT,
HWND hWndParent = 0,
HMENU hMenu = 0
)
{
WNDCLASS wc = { 0 };
wc.lpfnWndProc = DERIVED_TYPE::WindowProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = ClassName();
RegisterClass(&wc);
m_hwnd = CreateWindowEx(
dwExStyle, ClassName(), lpWindowName, dwStyle, x, y,
nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this
);
return (m_hwnd ? TRUE : FALSE);
}
//returns handle
HWND Window() const { return m_hwnd; }
protected:
virtual PCWSTR ClassName() const = 0;
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0;
HWND m_hwnd;
};
CreateBoard() method creates the Gems in the board.
BOOL MainWindow::CreateBoard()
{
for (unsigned int i = 0; i < GetcGem(); i++)
{
for (unsigned int j = 0; j < GetcGem(); j++)
{
if (Gems[i][j].Create(L"gem", WS_CHILDWINDOW | WS_VISIBLE, NULL,
5 + i * GetsGem().cx + i * 10,
5 + j * GetsGem().cy + j * 10, GetsGem().cx, GetsGem().cy, Window(), NULL))
{
//pass from main window to Gem object
Gems[i][j].SetSize(GetsGem().cx, GetsGem().cy);
Gems[i][j].SetPosition(5 + i * GetsGem().cx + i * 10, 5 + j * GetsGem().cy + j * 10);
HBRUSH hbrush = CreateSolidBrush(RGB(125,125,125));
HBRUSH hOldBrush = (HBRUSH)SetClassLongPtr(Gems[i][j].Window(), GCLP_HBRBACKGROUND, (LONG_PTR)hbrush);
DeleteObject(hOldBrush);
InvalidateRect(Gems[i][j].Window(), NULL, TRUE);
}
else
{
return FALSE;
}
}
}
return TRUE;
}
I want each child window to increase their size by 4 pixels on mouse hover separately. However, after expanding the size, they leave a trace, and sometimes hovering on a single-window increases the size of the whole row or column. Referer to Image, for example:
I have the handling code for each child window as below. tracking is a static BOOL in the GemWindow class
BOOL GemWindow::tracking = false;
LRESULT GemWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
DestroyWindow(Window());
break;
case WM_MOUSEMOVE:
{
if (!tracking) {
TrackMouse();
tracking = true;
}
}break;
case WM_MOUSEHOVER:
{
MoveWindow(Window(), GetSize().cx + 4, GetSize().cy + 4, GetPosition().x - 2, GetPosition().y - 2, TRUE);
InvalidateRect(Window(), NULL, TRUE);
OutputDebugString(L"MOUSE ENTERED\n");
}break;
case WM_MOUSELEAVE:
{
MoveWindow(Window(), GetSize().cx, GetSize().cy, GetPosition().x, GetPosition().y, TRUE);
InvalidateRect(Window(), NULL, TRUE);
OutputDebugString(L"MOUSE LEFT\n");
tracking = false;
}break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(m_hwnd, &ps);
//FillRect(hdc, &ps.rcPaint, CreateSolidBrush(color));
EndPaint(m_hwnd, &ps);
//OutputDebugString(L"PAINT\n");
}
return 0;
default:
return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}
return TRUE;
}
TrackMouse() is a method of GemWindow which initialize a TRACKMOUSEEVENTand call TrackMouseEvent()
//method for tracking mouse
void TrackMouse(/*reusability enhanced if you add arguments in the future*/)
{
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(TRACKMOUSEEVENT);
tme.dwFlags = TME_HOVER | TME_LEAVE; //Type of events to track & trigger.
tme.dwHoverTime = 1; //How long the mouse has to be in the window to trigger a hover event.
tme.hwndTrack = Window();
TrackMouseEvent(&tme);
}
I thought it is because I am not Updating regions, but doing that did not change anything. I am confused about this bug and can't think of any good reason for it.
Thanks to all who take their time to help.
BOOL MoveWindow(
HWND hWnd,
int X, //The new position of the left side of the window.
int Y, //The new position of the top of the window.
int nWidth, //The new width of the window.
int nHeight, //The new height of the window.
BOOL bRepaint
);
Modify the following code:
case WM_MOUSEHOVER:
{
// MoveWindow(Window(), GetSize().cx + 4, GetSize().cy + 4, GetPosition().x - 2, GetPosition().y - 2, TRUE);
MoveWindow(Window(), GetPosition().x - 2, GetPosition().y - 2, GetSize().cx + 4, GetSize().cy + 4, TRUE);
...
case WM_MOUSELEAVE:
{
// MoveWindow(Window(), GetSize().cx, GetSize().cy, GetPosition().x, GetPosition().y, TRUE);
MoveWindow(Window(), GetPosition().x, GetPosition().y, GetSize().cx, GetSize().cy, TRUE);
...
Debug:

Struggling to create a functional custom Windows 10 frame decoration following 'Custom Window Frame Using DWM'

I'm going to risk being shot down for being an idiot but I've sunk hours into trying to follow this windows tutorial with no luck: Custom Window Frame Using DWM
My main goal is to create a custom window frame that behaves in the same was as the default one (for example, can be maximised by being dragged to the top of the screen, has the normal minimise, maximise, exit buttons) but one that sports a different colour and perhaps some menu items within the frame. I'm fairly comfortable using wxWidgets but I am an absolute beginner (if even a beginner) using the Windows API. Unfortunately I've been led here anyway because it looks like the only way to achieve what I'm after.
Going back to the abovementioned linked tutorial, I've able to extend the frame using DwmExtendFrameIntoClientArea. This yields a result that looks like this on Windows 10 using a grey brush:
My first point of confusion happens when trying to follow the instructions under "Removing the Standard Frame". This is what I get when I try and follow the code example:
This looks similar enough to the example image in the tutorial (below) but all of the window functionality is now gone! I.e. clicking any of the top-right three buttons doesn't nothing and the window cannot be clicked, moved or resized.
I had hoped that this functionality would return once I'd added in the rest of the example code, which I won't reproduce as they are contained in the page I linked under Appendix B and C (titled 'Painting the Caption Title' and 'HitTestNCA Function' respectively).
Not only did the functionality not return, the example code didn't seem to do anything... and I ended up with the window as it was after my previous edit (pictured below - to be clear):
I haven't reproduced by code here because it's exactly the same as the code found in the link except the background colour is changed to grey and I've added a static 'Test widget' to give myself a point of reference for what the coordinates were doing.
If any kind soul could please tell me what I am doing wrong, or whether my goals are even achievable using the methods I've reluctantly chosen, I would greatly appreciate some advice.
Many thanks!
I created a project according to the documentation and did not reproduce this issue, but you can compare it with your project:
#include <windows.h>
#include <stdio.h>
#include <uxtheme.h>
#include <dwmapi.h>
#include <vssym32.h>
#include <windowsx.h>
#pragma comment(lib, "dwmapi.lib")
#pragma comment(lib, "uxtheme.lib")
#define RECTWIDTH(rc) (rc.right - rc.left)
#define RECTHEIGHT(rc) (rc.bottom - rc.top)
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT AppWinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT CustomCaptionProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, bool* pfCallDWP);
void PaintCustomCaption(HWND hWnd, HDC hdc);
LRESULT HitTestNCA(HWND hWnd, WPARAM wParam, LPARAM lParam);
HWND createmainwindow()
{
WNDCLASSEXW wcex = { 0 };
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = GetModuleHandle(NULL);
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
wcex.lpszClassName = L"My_Class";
RegisterClassExW(&wcex);
HWND hWnd = CreateWindowW(wcex.lpszClassName, L"Test", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, wcex.hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
HWND staticctrl = CreateWindowW(L"STATIC", L"SETTINGS", SS_LEFT | WS_CHILD,
8, 27, 500, 300, hWnd, NULL, wcex.hInstance, NULL);
if (!staticctrl)
{
return FALSE;
}
ShowWindow(hWnd, SW_NORMAL);
UpdateWindow(hWnd);
ShowWindow(staticctrl, SW_NORMAL);
UpdateWindow(staticctrl);
}
void main()
{
HWND hWnd = createmainwindow();
MSG msg;
// Main message loop:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, 0, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
bool fCallDWP = true;
BOOL fDwmEnabled = FALSE;
LRESULT lRet = 0;
HRESULT hr = S_OK;
// Winproc worker for custom frame issues.
hr = DwmIsCompositionEnabled(&fDwmEnabled);
if (SUCCEEDED(hr))
{
lRet = CustomCaptionProc(hWnd, message, wParam, lParam, &fCallDWP);
}
// Winproc worker for the rest of the application.
if (fCallDWP)
{
lRet = AppWinProc(hWnd, message, wParam, lParam);
}
return lRet;
}
//
// Message handler for the application.
//
LRESULT AppWinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
HRESULT hr;
LRESULT result = 0;
switch (message)
{
case WM_CREATE:
{}
break;
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
PaintCustomCaption(hWnd, hdc);
// Add any drawing code here...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// Message handler for handling the custom caption messages.
//
LRESULT CustomCaptionProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, bool* pfCallDWP)
{
LRESULT lRet = 0;
HRESULT hr = S_OK;
bool fCallDWP = true; // Pass on to DefWindowProc?
fCallDWP = !DwmDefWindowProc(hWnd, message, wParam, lParam, &lRet);
// Handle window creation.
if (message == WM_CREATE)
{
RECT rcClient;
GetWindowRect(hWnd, &rcClient);
// Inform application of the frame change.
SetWindowPos(hWnd,
NULL,
rcClient.left, rcClient.top,
RECTWIDTH(rcClient), RECTHEIGHT(rcClient),
SWP_FRAMECHANGED);
fCallDWP = true;
lRet = 0;
}
// Handle window activation.
if (message == WM_ACTIVATE)
{
// Extend the frame into the client area.
MARGINS margins;
margins.cxLeftWidth = 8; // 8
margins.cxRightWidth = 8; // 8
margins.cyBottomHeight = 20; // 20
margins.cyTopHeight = 27; // 27
hr = DwmExtendFrameIntoClientArea(hWnd, &margins);
if (!SUCCEEDED(hr))
{
// Handle error.
}
fCallDWP = true;
lRet = 0;
}
if (message == WM_PAINT)
{
HDC hdc;
{
PAINTSTRUCT ps;
hdc = BeginPaint(hWnd, &ps);
PaintCustomCaption(hWnd, hdc);
EndPaint(hWnd, &ps);
}
fCallDWP = true;
lRet = 0;
}
// Handle the non-client size message.
if ((message == WM_NCCALCSIZE) && (wParam == TRUE))
{
// Calculate new NCCALCSIZE_PARAMS based on custom NCA inset.
NCCALCSIZE_PARAMS* pncsp = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);
pncsp->rgrc[0].left = pncsp->rgrc[0].left + 0;
pncsp->rgrc[0].top = pncsp->rgrc[0].top + 0;
pncsp->rgrc[0].right = pncsp->rgrc[0].right - 0;
pncsp->rgrc[0].bottom = pncsp->rgrc[0].bottom - 0;
lRet = 0;
// No need to pass the message on to the DefWindowProc.
fCallDWP = false;
}
//Handle hit testing in the NCA if not handled by DwmDefWindowProc.
if ((message == WM_NCHITTEST) && (lRet == 0))
{
lRet = HitTestNCA(hWnd, wParam, lParam);
if (lRet != HTNOWHERE)
{
fCallDWP = false;
}
}
*pfCallDWP = fCallDWP;
return lRet;
}
// Paint the title on the custom frame.
void PaintCustomCaption(HWND hWnd, HDC hdc)
{
RECT rcClient;
GetClientRect(hWnd, &rcClient);
HTHEME hTheme = OpenThemeData(NULL, L"CompositedWindow::Window");
if (hTheme)
{
HDC hdcPaint = CreateCompatibleDC(hdc);
if (hdcPaint)
{
int cx = RECTWIDTH(rcClient);
int cy = RECTHEIGHT(rcClient);
// Define the BITMAPINFO structure used to draw text.
// Note that biHeight is negative. This is done because
// DrawThemeTextEx() needs the bitmap to be in top-to-bottom
// order.
BITMAPINFO dib = { 0 };
dib.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
dib.bmiHeader.biWidth = cx;
dib.bmiHeader.biHeight = -cy;
dib.bmiHeader.biPlanes = 1;
dib.bmiHeader.biBitCount = 32;
dib.bmiHeader.biCompression = BI_RGB;
HBITMAP hbm = CreateDIBSection(hdc, &dib, DIB_RGB_COLORS, NULL, NULL, 0);
if (hbm)
{
HBITMAP hbmOld = (HBITMAP)SelectObject(hdcPaint, hbm);
// Setup the theme drawing options.
DTTOPTS DttOpts = { sizeof(DTTOPTS) };
DttOpts.dwFlags = DTT_COMPOSITED | DTT_GLOWSIZE;
DttOpts.iGlowSize = 15;
// Select a font.
LOGFONT lgFont;
HFONT hFontOld = NULL;
if (SUCCEEDED(GetThemeSysFont(hTheme, TMT_CAPTIONFONT, &lgFont)))
{
HFONT hFont = CreateFontIndirect(&lgFont);
hFontOld = (HFONT)SelectObject(hdcPaint, hFont);
}
// Draw the title.
RECT rcPaint = rcClient;
rcPaint.top += 8;
rcPaint.right -= 125;
rcPaint.left += 8;
rcPaint.bottom = 50;
DrawThemeTextEx(hTheme,
hdcPaint,
0, 0,
L"Test",
-1,
DT_LEFT | DT_WORD_ELLIPSIS,
&rcPaint,
&DttOpts);
// Blit text to the frame.
BitBlt(hdc, 0, 0, cx, cy, hdcPaint, 0, 0, SRCCOPY);
SelectObject(hdcPaint, hbmOld);
if (hFontOld)
{
SelectObject(hdcPaint, hFontOld);
}
DeleteObject(hbm);
}
DeleteDC(hdcPaint);
}
CloseThemeData(hTheme);
}
}
// Hit test the frame for resizing and moving.
LRESULT HitTestNCA(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
// Get the point coordinates for the hit test.
POINT ptMouse = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
// Get the window rectangle.
RECT rcWindow;
GetWindowRect(hWnd, &rcWindow);
// Get the frame rectangle, adjusted for the style without a caption.
RECT rcFrame = { 0 };
AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);
// Determine if the hit test is for resizing. Default middle (1,1).
USHORT uRow = 1;
USHORT uCol = 1;
bool fOnResizeBorder = false;
// Determine if the point is at the top or bottom of the window.
if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + 27)
{
fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top));
uRow = 0;
}
else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - 20)
{
uRow = 2;
}
// Determine if the point is at the left or right of the window.
if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + 8)
{
uCol = 0; // left side
}
else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - 8)
{
uCol = 2; // right side
}
// Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT)
LRESULT hitTests[3][3] =
{
{ HTTOPLEFT, fOnResizeBorder ? HTTOP : HTCAPTION, HTTOPRIGHT },
{ HTLEFT, HTNOWHERE, HTRIGHT },
{ HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT },
};
return hitTests[uRow][uCol];
}
Then,
the window cannot be clicked, moved or resized.
We cannot expect that without implementing logic to handle caption button hit testing and frame resizing/moving.
You will get the black of the background color default(BITMAPINFO dib = { 0 };). But the screen shot was still gray. (This may be because the function didn't work (failed?). Or, when you test the code before, you've commented B, C , and then when you add B, C, didn't to uncomment it).