0

I'm trying to create a Label class where I can just reuse it later. What I've done is create a static control, then use the GDI+ library to DrawString on it.

It's almost done, I only have one issue where I need to automatically set the width and height of the static control to fit the text on it.

/** Call this function to redraw the content of static control **/
void ControlLabel::UpdateLabel() {
    if(LabelHandle != NULL) {
        SetWidthAndHeight();
        SetWindowPos(LabelHandle, nullptr, xPosition, yPosition, width, height,  SWP_NOZORDER | SWP_NOOWNERZORDER);
        InvalidateRect(LabelHandle, NULL, FALSE);
        UpdateWindow(LabelHandle);
    }
}
/** THis function is the callback of the static control **/
LRESULT CALLBACK ControlLabel::LabelProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) {
    switch(uMsg) {
    case WM_ERASEBKGND: {
            if(SetBGColor) { //We only want to do this if the SetColor is modified to true, meaning we want to set the color of background.
                RECT rect;
                GetClientRect(hwnd, &rect);
                FillRect((HDC)wParam, &rect, CreateSolidBrush(RGB(BckR, BckG, BckB))); //set titlebar background color.
                return 1; //return 1, meaning we take care of erasing the background.
            }
            return 0;
        }case WM_PAINT: {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);
            Graphics g(hdc);

            std::wstring widestr;
            widestr = std::wstring(vFontFamily.begin(), vFontFamily.end());

            FontFamily  theFontFamily(widestr.c_str());
            Font        font(&theFontFamily, vFontSize, FontStyleRegular, UnitPixel);
            SolidBrush  brush(Color(255, R, G, B));
            PointF      pointF(0.0f, 0.0f);

            TextRenderingHint hint = g.GetTextRenderingHint(); // Get the text rendering hint.
            g.SetTextRenderingHint(TextRenderingHintAntiAlias); // Set the text rendering hint to TextRenderingHintAntiAlias. 

            widestr = std::wstring(text.begin(), text.end());  // convert text to std::wstring:
            g.DrawString(widestr.c_str(), -1, &font, pointF, &brush);       // get the C string

            EndPaint(hwnd, &ps);
            return TRUE;
        }case WM_NCDESTROY: {
            RemoveWindowSubclass(hwnd, LabelProc, uIdSubclass);
            return 0;
        }
    }
    return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}

/** Use this function to create a Label. Parent or WindowHandle must be specify, this is where the Label will be draw. Unique Label ID must be specify. **/
HWND ControlLabel::Label(int Label_ID, HWND WindowHandle) {
    SetWidthAndHeight();
    LabelHandle = CreateWindowEx(0, L"STATIC", NULL, WS_CHILD | WS_VISIBLE | SS_OWNERDRAW, 0, 0, width, height, WindowHandle, NULL, NULL, NULL); //create the static control.
    SetWindowSubclass(LabelHandle, &LabelProc, LABEL_ID, 0);
    return LabelHandle;
}

I thought of using GetTextExtentPoint32 to calculate the height and width of the string, unfortunately, I failed to do so because of the font size and font family.

void ControlLabel::SetWidthAndHeight() {
    std::wstring stemp = StringConverter(vFontFamily);
    LPCWSTR result = stemp.c_str();

    HDC hdc = GetDC(LabelHandle);//static control
    const wchar_t* buf = L"Hello World, this is 25 font size.";
    /*//(font test 1)
       HFONT hFont = CreateFont(vFontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
    OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, result);
    */

    //(font test 2)
    FontFamily  theFontFamily(result);
    Font        font(&theFontFamily, vFontSize, FontStyleRegular, UnitPixel);

    SIZE size;
    HFONT oldfont = (HFONT)SelectObject(hdc, &font);
    GetTextExtentPoint32(hdc, buf, wcslen(buf), &size);
    width = size.cx;
    height = size.cy;

    SelectObject(hdc, oldfont);
    DeleteObject(&font);
    ReleaseDC(LabelHandle, hdc);  
}

How should I fix it?

UPDATE

Here's the complete source code of my class.

ControlLabel.cpp

#include "ControlLabel.h"

HWND ControlLabel::LabelHandle = NULL;
int ControlLabel::xPosition = 0;
int ControlLabel::yPosition = 0;
int ControlLabel::width = 0;
int ControlLabel::height = 0;
int ControlLabel::LABEL_ID = 0;
int ControlLabel::vFontSize = 12;
int ControlLabel::R = 0;
int ControlLabel::G = 0;
int ControlLabel::B = 0;
int ControlLabel::BckR = 0;
int ControlLabel::BckG = 0;
int ControlLabel::BckB = 0;
bool ControlLabel::SetBGColor = FALSE;
string ControlLabel::text = "Label";
string ControlLabel::vFontFamily = "Segoe UI";

ControlLabel::ControlLabel() {}

/** This function is used to convert string into std::wstring. **/
std::wstring ControlLabel::StringConverter(const std::string& s) {
    int len;
    int slength = (int)s.length() + 1;
    len = MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, 0, 0);
    wchar_t* buf = new wchar_t[len];
    MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, buf, len);
    std::wstring r(buf);
    delete[] buf;
    return r;
}

/** This function is used to automatically set the Width and Height of static control base on the length of the text. **/
void ControlLabel::SetWidthAndHeights() {    
    std::wstring fontFamilyTemp = StringConverter(vFontFamily);
    std::wstring  textTemp = StringConverter(text);
    LPCWSTR textLabel = textTemp.c_str();
    
    HDC hdc = GetDC(LabelHandle);//static control
    const wchar_t* buf = L"Hello World, this is 25 font size.";

    HFONT hFont = CreateFont(
          -MulDiv(vFontSize, GetDeviceCaps(hdc, LOGPIXELSX), 90),
          0, 0, 0, // normal orientation
          FW_NORMAL,   // normal weight--e.g., bold would be FW_BOLD
          false, false, false, // not italic, underlined or strike out
          DEFAULT_CHARSET, OUT_OUTLINE_PRECIS, // select only outline (not bitmap) fonts
          CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, VARIABLE_PITCH | FF_SWISS, fontFamilyTemp.c_str());

    SIZE size;
    HFONT oldfont = (HFONT)SelectObject(hdc, hFont);
    GetTextExtentPoint32(hdc, textLabel, wcslen(textLabel), &size);
    width = size.cx;
    height = size.cy;

    SelectObject(hdc, oldfont);
    DeleteObject(hFont);
    ReleaseDC(LabelHandle, hdc);

    char buffer[100];
    sprintf_s(buffer, "WIDTH: %d | HEIGHT: %d\n", width, height);
    OutputDebugStringA(buffer);
}

/** This function will be called when new option is set. For example, fontSize is set. **/
void ControlLabel::UpdateLabel() {
    if(LabelHandle != NULL) {
        SetWidthAndHeights();
        SetWindowPos(LabelHandle, nullptr, xPosition, yPosition, width, height, SWP_NOZORDER | SWP_NOOWNERZORDER);
        InvalidateRect(LabelHandle, NULL, FALSE);
        UpdateWindow(LabelHandle);
    }
}

/** This is the callback function of static control. **/
LRESULT CALLBACK ControlLabel::LabelProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) {
    switch(uMsg) {
        case WM_ERASEBKGND: {
            if(SetBGColor) { //We only want to do this if the SetColor is modified to true, meaning we want to set the color of background.
                RECT rect;
                GetClientRect(hwnd, &rect);
                FillRect((HDC)wParam, &rect, CreateSolidBrush(RGB(BckR, BckG, BckB))); //set titlebar background color.
                return 1; //return 1, meaning we take care of erasing the background.
            }
            return 0;
        }case WM_PAINT: {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);
            Graphics g(hdc);

            std::wstring fontFamilyTemp = StringConverter(vFontFamily);
            std::wstring  textTemp = StringConverter(text);

            FontFamily  theFontFamily(fontFamilyTemp.c_str());
            Font        font(&theFontFamily, vFontSize, FontStyleRegular, UnitPixel);
            SolidBrush  brush(Color(255, R, G, B));
            PointF      pointF(0.0f, 0.0f);

            TextRenderingHint hint = g.GetTextRenderingHint(); // Get the text rendering hint.
            g.SetTextRenderingHint(TextRenderingHintAntiAlias); // Set the text rendering hint to TextRenderingHintAntiAlias. 
            g.DrawString(textTemp.c_str(), -1, &font, pointF, &brush); 

            EndPaint(hwnd, &ps);
            return TRUE;
        }case WM_NCDESTROY: {
            RemoveWindowSubclass(hwnd, LabelProc, uIdSubclass);
            return 0;
        }
    }
    return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}

/** Use this function to create a Label. Parent or WindowHandle must be specify, this is where the Label will be draw. Unique Label ID must be specify. **/
HWND ControlLabel::Label(int Label_ID, HWND WindowHandle) {
    LABEL_ID = Label_ID;
    LabelHandle = CreateWindowEx(0, L"STATIC", NULL, WS_CHILD | WS_VISIBLE | SS_OWNERDRAW, 0, 0, width, height, WindowHandle, NULL, NULL, NULL); //create the static control.
    SetWindowSubclass(LabelHandle, &LabelProc, LABEL_ID, 0);
    return LabelHandle;
}

/** Use this function to set the X Position of the Label. **/
void ControlLabel::SetXPosition(int xxPosition) {
    if(LabelHandle != NULL) {
        xPosition = xxPosition; //set xposition
        UpdateLabel();
    }
}

/** Use this function to set the Y Position of the Label. **/
void ControlLabel::SetYPosition(int yyPosition) {
    if(LabelHandle != NULL) {
        yPosition = yyPosition; //set xposition
        UpdateLabel();
    }
}

/** Use this function to set the text of the Label. **/
void ControlLabel::SetText(string ttext) {
    if(LabelHandle != NULL) {
        text = ttext; //set text
        UpdateLabel();
    }
}

/** Use this function to set the font family of the Label. **/
void ControlLabel::SetFontFamily(string font_family) {
    if(LabelHandle != NULL) {
        vFontFamily = font_family; //set font family
        UpdateLabel();
    }
}

/** Use this function to set the font size of the Label. **/
void ControlLabel::SetFontSize(int size) {
    if(LabelHandle != NULL) {
        vFontSize = size; //set font size
        UpdateLabel();
    }
}

/** Use this Function to set the font color of the Label using RGB. **/
void ControlLabel::SetFontColor(int Rr, int Gg, int Bb) {
    if(LabelHandle != NULL) {
        R = Rr; 
        G = Gg; 
        B = Bb; 
        UpdateLabel();
    }
}

/** Use this Function to set the background color of the Label using RGB. Last parameter must be TRUE if you want to set your own background color. **/
void ControlLabel::SetBackgroundColor(int Rr, int Gg, int Bb, bool setColor) {
    if(LabelHandle != NULL) {
        SetBGColor = setColor;
        BckR = Rr;
        BckG = Gg;
        BckB = Bb;
        UpdateLabel();
    }
}

ControlLabel.h

#pragma once

#ifndef CONTROLLABEL_H
#define CONTROLLABEL_H
#include "Header.h"

class ControlLabel {

public:
    ControlLabel();
    HWND Label(int Label_ID, HWND WindowHandle);
    void SetXPosition(int xPosition);
    void SetYPosition(int yPosition);
    void SetText(string Text);
    void SetFontFamily(string FontFamily);
    void SetFontSize(int FontSize);
    void SetFontColor(int R, int G, int B);
    void SetBackgroundColor(int Rr, int Gg, int Bb, bool SetBGColor);

private:
    void UpdateLabel();
    void SetWidthAndHeights();
    static std::wstring StringConverter(const std::string& s);
    static LRESULT CALLBACK LabelProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
    static HWND LabelHandle;
    static SolidBrush vFontColor;
    static string text, vFontFamily;
    static bool SetBGColor;
    static int xPosition, yPosition, width, height, LABEL_ID, vFontSize, R, G, B, BckR, BckG, BckB;
};

#endif

Header.h

#pragma once

#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") 

#include <stdexcept>
#include <system_error>

#include <Windows.h>
#include <commctrl.h>
#include <windowsx.h>
#include <dwmapi.h>
#include <tchar.h>
#include <string>
#include <thread>
#include <chrono>

#include <objidl.h>
#include <gdiplus.h>
using namespace std;
using namespace Gdiplus;
#pragma comment (lib,"Gdiplus.lib")
#pragma comment(lib, "dwmapi.lib")
#pragma comment(lib, "comctl32.lib")

Then on my main window, I used it like this.

case WM_CREATE:{  
    ControlLabel controlLabel;
    controlLabel.Label(123, hwnd); //create the label.
    controlLabel.SetXPosition(10); //set the x position.
    controlLabel.SetYPosition(10); //set the x position.
    controlLabel.SetText("Hello World, this is 25 font size."); //set the text.
    controlLabel.SetFontSize(20); //set the font size.
    controlLabel.SetFontFamily("Calibri"); //set the font family.
    controlLabel.SetFontColor(15, 86, 209); //set the font color.
    controlLabel.SetBackgroundColor(220, 222, 224, true); //set the background color.
    
    /**
        I'm still planning to add more options like italic, bold, etc.
    **/
}

The suggested solution of Polar works, but I don't know if that's the correct way. I'm still reading the given documentation.

Papilion
  • 87
  • 2
  • 10
  • From the [documentation](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-gettextextentpoint32w): *"The `GetTextExtentPoint32` function uses the currently selected font to compute the dimensions of the string."* If you want to calculate the width based on a particular font, select that font into the device context. That doesn't fix another glaring issue: Inability to display anything that's not composed of ASCII characters only. `wstring(text.begin(), text.end())` doesn't convert. – IInspectable Mar 16 '21 at 09:32
  • Since you're already using GDI+, you could use the [Graphics::MeasureString](https://learn.microsoft.com/en-us/previous-versions//ms535831(v=vs.85)) method. – ericpat Mar 16 '21 at 14:49
  • `GetTextExtentPoint32` will select the current font to calculate the size of the string. You need to select the correct device context. – Zeus Mar 17 '21 at 01:51
  • I updated the `SetWidthAndHeight()` function but still not getting the correct width and height. – Papilion Mar 18 '21 at 02:49
  • Your `(font test 1)` is almost correct, I think the problem is your `vFontSize`. If you want a 25 font size and the value of the `vFontSize` is also 25 and you do `CreateFont(vFontSize....)` then the calculated size will be different from what you expect. See the documentation of it https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createfonta – Polar Mar 18 '21 at 07:32
  • Instead of the 25, you can do `nHeight = -MulDiv(PointSize, GetDeviceCaps(hDC, LOGPIXELSY), 72);`. Then you can do `CreateFont(nHeight, 0,0, 0....)`. – Polar Mar 18 '21 at 07:33
  • What is PointSize? – Papilion Mar 18 '21 at 08:23
  • I tried `-MulDiv(vFontSize, GetDeviceCaps(hDC, LOGPIXELSY), 72);`, but still not the expected size, but almost. – Papilion Mar 18 '21 at 08:30
  • Would you mind posting the whole class so we know what exactly is happening? – Polar Mar 18 '21 at 08:42
  • Try playing around with the denominator of `MulDiv`. `MulDiv(int nNumber,int nNumerator,int nDenominator);`, try using `90` or something else. Refer to the documentation for more details. https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-muldiv – Polar Mar 18 '21 at 08:46
  • Another thing is your string conversation is very wrong. See the accepted answer here https://stackoverflow.com/questions/27220/how-to-convert-stdstring-to-lpcwstr-in-c-unicode – Polar Mar 18 '21 at 08:50
  • @Polar - I tried using `90` and it does work, but I still have a doubt about it. Why does it work? Also, I tried to use the accepted solution on that given link, thank you for telling me that. I posted the whole class, I hope it helps the future researcher. – Papilion Mar 18 '21 at 09:05
  • If that's the correct way and nothing else, would you mind posting your comment as an answer? I'd like to mark it as an accepted answer. Thank you. – Papilion Mar 18 '21 at 09:07
  • Does this answer your question? [C++ GetTextExtentPoint32 doesn't give the correct size](https://stackoverflow.com/questions/66795957/c-gettextextentpoint32-doesnt-give-the-correct-size) – Polar Mar 31 '21 at 06:57

1 Answers1

0

There are two ways to computes the width and height of the specified string of text. You can use neither GetTextExtentPoint32 and Graphics::MeasureString to get the actual size depending on how you drew your text.

In your case, you are drawing the text using GDI+ but you are measuring the width and height of it using the classic GDI which will give you a different result because of scaling. Although you can still use GetTextExtentPoint32 to measure the text. However, you will need to handle the DPI to get the width and height the same as how it is drawn using GDI+. GDI+ is an improvement on GDI and there are differences between the two. For example, the scaling on how they draw a string.

The following code shows how to draw the string using Graphics::DrawString and compute the width and height using Graphics::MeasureString. Drawing and computation are done using GDI+.

/** Draw the string using GDI+ Graphics::DrawString **/
void DrawString(){
    HDC hdc = GetDC(hwnd);                                              //get dc of your handle.
    Graphics g(hdc);
    
    FontFamily  fontFamily("Arial Black");                              // Set font family.
    Font        font(&fontFamily, 12, FontStyleRegular, UnitPixel);     // Create the font.
    SolidBrush  brush(Color(255, 0, 0, 0));                             // Set font color
    PointF      pointF(0.0f, 0.0f);                                     // Set X and Y position.

    TextRenderingHint hint = g.GetTextRenderingHint();                  // Get the text rendering hint.
    g.SetTextRenderingHint(TextRenderingHintAntiAlias);                 // Make sure the rendering is high quality. 
    g.DrawString(L"Hello World", -1, &font, pointF, &brush);            // Draw the string.
    
    DeleteObject(&font);                                                // always delete the font when done.
    DeleteObject(&brush);                                               // always delete the brush when done.
    ReleaseDC(LabelHandle, hdc);                                        // always release the DC when done.
}

/** Measure the string using GDI+ Graphics::MeasureString **/
void MeasureString(){
    HDC hdc = GetDC(hwnd);                                                      // get dc of your handle.
    Graphics graphics(hdc);                                                     // setup graphics.
    FontFamily  theFontFamily(vFontFamily);                                     // setup your font family.
    Font        font(&theFontFamily, vFontSize, FontStyleRegular, UnitPixel);   // create the font the same way how you do it on your paint message.
    PointF      pointF(0.0f, 0.0f);                                             // use PointF instead of RectF since thats how you paint it.

    RectF boundRect;                                                            // setup boundRect to get the width and height.
    graphics.MeasureString(text, -1, &font, pointF, &boundRect);                // Measure the text of the string

    int width = boundRect.Width;                                                // get the width of text from boundRect.
    in height = boundRect.Height;                                               // get the height of text from boundRect.

    DeleteObject(&font);                                                        // delete the font.
    ReleaseDC(LabelHandle, hdc);                                           // Release the DC.
}

Another example shows how to draw the string using the classic GDI and compute the width and height using GetTextExtenPoint32. Drawing and computation are done using the classic GDI.

/** Draw the string using the classic GDI DrawText **/
void DrawString(){
    int fontSize = 12;                                                  // Font Size.
    HDC hdc = GetDC(hwnd);                                              // Get DC of your handle.
    HFONT hFont = CreateFont(
         -MulDiv(fontSize, GetDeviceCaps(hdc, LOGPIXELSX), 72),         // Calculate the actual cHeight.
         0, 0, 0,                                                       // Normal orientation
         FW_NORMAL,                                                     // Normal weight--e.g., bold would be FW_BOLD, or use integer from 0 to 1000.
         false, false, false,                                           // Not italic, underlined or strike out
         DEFAULT_CHARSET, OUT_OUTLINE_PRECIS,                           // select only outline (not bitmap) fonts
         CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, VARIABLE_PITCH | FF_SWISS,
         TEXT("Arial Black"));                                          // Font family.
   
    HFONT oldfont = (HFONT)SelectObject(hdc, hFont);                    // Select the new font.
    RECT      rect = {0, 0, 200, 50};                                   // Rectangle (in logical coordinates) in which the text is to be formatted.
    
    DrawText(hdc, L"Helow World", -1, &rect, DT_CENTER | DT_VCENTER);   // Draw the text Horizontal and Vertical Center.
   
    SelectObject(hdc, oldfont);                                         // don't forget to select the old.
    DeleteObject(hFont);                                                // always delete the font when done.
    ReleaseDC(hwnd, hdc);                                               // always release dc after using.
}

/** Measure the string using the classic GDI GetTextExtentPoint32 **/
void MeasureString(){
    HDC hdc = GetDC(hwnd);
    HFONT hFont = CreateFont(                                           // Create the font.
    int fontSize = 12;                                                  // Font Size.
    HDC hdc = GetDC(hwnd);                                              // Get DC of your handle.
    HFONT hFont = CreateFont(
         -MulDiv(fontSize, GetDeviceCaps(hdc, LOGPIXELSX), 72),         // Calculate the actual cHeight.
         0, 0, 0,                                                       // Normal orientation
         FW_NORMAL,                                                     // Normal weight--e.g., bold would be FW_BOLD, or use integer from 0 to 1000.
         false, false, false,                                           // Not italic, underlined or strike out
         DEFAULT_CHARSET, OUT_OUTLINE_PRECIS,                           // select only outline (not bitmap) fonts
         CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, VARIABLE_PITCH | FF_SWISS,
         TEXT("Arial Black"));                                          // Font family.
   
    HFONT oldfont = (HFONT)SelectObject(hdc, hFont);                    // Select the new font.
    SIZE size;                                                          // Setup Size to get the width and height.
    GetTextExtentPoint32(hdc, L"Hello World", -1, &size);               // Draw the text Horizontal and Vertical Center.

    int width = size.cx;                                                // get the width of text from boundRect.
    int height = size.cy;                                               // get the height of text from boundRect.    

    SelectObject(hdc, oldfont);                                         // don't forget to select the old.
    DeleteObject(hFont);                                                // always delete the font when done.
    ReleaseDC(hwnd, hdc);                                               // always release dc after using.
}

Since you are already using GID+ to draw the text, then it is recommended to use Graphics::MeasureString to compute the size of it, this will avoid the headache of getting the correct DPI. You must get the size of the text the same as how you drew it. Then with this method, you will be able to set the width and height of your static control automatically.

For example on your UpdateLabel();

/** Call this function to redraw the content of static control **/
void ControlLabel::UpdateLabel() {
    if(LabelHandle != NULL) {
        MeasureString(); //Measure string to get the width and height of it and adjust the size of static control depending on the size of the text.
        SetWindowPos(LabelHandle, nullptr, xPosition, yPosition, width, height,  SWP_NOZORDER | SWP_NOOWNERZORDER);
        InvalidateRect(LabelHandle, NULL, FALSE);
        UpdateWindow(LabelHandle);
    }
}
Polar
  • 3,327
  • 4
  • 42
  • 77