TEXTMETRIC is giving wrong height when resizing text with mouse wheel - winapi

I'm calculating the number of lines in a rich edit control.
Currently I'm using next code
TEXTMETRIC tm; {
HDC hdc = GetDC(hwndRichEdit);
GetTextMetrics(hdc, &tm);
ReleaseDC(hwndRichEdit, hdc);
}
RECT editRect;
GetClientRect(hwndRichEdit, &editRect);
long int countLines = (editRect.bottom - editRect.top) / (tm.tmHeight + tm.tmExternalLeading);
The code yields out the right number of lines until I start to change the size of the text via mouse wheel + ctr.
Is it possible to get the right text height even if the text is resized with mouse wheel?
N.B. I'm recalculating the number of lines with EN_UPDATE notification.

You can send an EM_GETZOOM message to the control to retrieve the current zoom ratio. Dividing the countLines value by the zoom ratio should yield the correct line count. Use the MulDiv API call to implement the division.

Related

GDI: Create Mountain Chart/Graph?

I can use Polyline() GDI function to plot values to create a graph but now I want the lower part of it filled in to create a mountain type chart. Is there something built-in to help create that? (I don't need gradient, but that would be a nice touch).
TIA!!
For this diagram type you need to draw a filled shape. The Polygon function can be used to draw an irregularly shaped filled object:
The polygon is outlined by using the current pen and filled by using the current brush [...].
The polygon points need to be constructed from the data points (left to right). To turn this into a closed shape, the bottom right and bottom left points of the diagram area need to be appended. The Polygon function then closes the shape automatically by drawing a line from the last vertex to the first.
The following implementation renders a single data set into a given device context's area:
/// \brief Renders a diagram into a DC
///
/// \param dc Device context to render into
/// \param area Diagram area in client coordinates
/// \param pen_col Diagram outline color
/// \param fill_col Diagram fill color
/// \param data Data points to render in data coordinate space
/// \param y_min Y-axis minimum in data coordinate space
/// \param y_max Y-axis maximum in data coordiante space
void render_diagram(HDC dc, RECT area,
COLORREF pen_col, COLORREF fill_col,
std::vector<int> const& data, int y_min, int y_max) {
// Make sure we have data
if (data.size() < 2) { return; }
// Make sure the diagram area isn't empty
if (::IsRectEmpty(&area)) { return; }
// Make sure the y-scale is sane
if (y_max <= y_min) { return; }
std::vector<POINT> polygon{};
// Reserve enough room for the data points plus bottom/right
// and bottom/left to close the shape
polygon.reserve(data.size() + 2);
auto const area_width{ area.right - area.left };
auto const area_height{ area.bottom - area.top };
// Translate coordinates from data space to diagram space
// In lieu of a `zip` view in C++ we're using a raw loop here
// (we need the index to scale the x coordinate, so we cannot
// use a range-based `for` loop)
for (int index{}; index < static_cast<int>(data.size()); ++index) {
// Scale x value
auto const x = ::MulDiv(index, area_width - 1, static_cast<int>(data.size()) - 1) + area.left;
// Flip y value so that the origin is in the bottom/left
auto const y_flipped = y_max - (data[index] - y_min);
// Scale y value
auto const y = ::MulDiv(y_flipped, area_height - 1, y_max - y_min);
polygon.emplace_back(POINT{ x, y });
}
// Semi-close the shape
polygon.emplace_back(POINT{ area.right - 1, area.bottom - 1 });
polygon.emplace_back(POINT{ area.left, area.bottom - 1 });
// Prepare the DC for rendering
auto const prev_pen{ ::SelectObject(dc, ::GetStockObject(DC_PEN)) };
auto const prev_brush{ ::SelectObject(dc, ::GetStockObject(DC_BRUSH)) };
::SetDCPenColor(dc, pen_col);
::SetDCBrushColor(dc, fill_col);
// Render the graph
::Polygon(dc, polygon.data(), static_cast<int>(polygon.size()));
// Restore DC (stock objects do not need to be destroyed)
::SelectObject(dc, prev_brush);
::SelectObject(dc, prev_pen);
}
Most of this function deals with translating and scaling data values into the target (client) coordinate space. The actual rendering is fairly compact in comparison, and starts from the comment reading // Prepare the DC for rendering.
To test this you can start from a standard Windows Desktop application, and dump the following into the WM_PAINT handler:
case WM_PAINT:
{
RECT rc{};
::GetClientRect(hWnd, &rc);
// Leave a 10px border around the diagram area
::InflateRect(&rc, -10, -10);
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
auto pen_col = RGB(0x00, 0x91, 0x7C);
auto fill_col = RGB(0xCC, 0xE9, 0xE4);
render_diagram(hdc, rc, pen_col, fill_col, g_dataset1, 0, 100);
pen_col = RGB(0x02, 0x59, 0x55);
fill_col = RGB(0xCC, 0xDD, 0xDD);
render_diagram(hdc, rc, pen_col, fill_col, g_dataset2, 0, 100);
EndPaint(hWnd, &ps);
}
break;
g_dataset1/g_dataset2 are containers holding random values that serve as test input. It is important to understand, that the final diagram is rendered back to front, meaning that data sets with smaller values need to be rendered after data sets with higher values; the lower portion gets repeatedly overdrawn.
This produces output that looks something like this:
Note that on a HiDpi display device GDI rendering gets auto-scaled. This produces the following:
Looking closely you'll observe that the lines are wider than 1 device pixel. If you'd rather have a more crisp look, you can disable DPI virtualization by declaring the application as DPI aware. Things don't change for a standard DPI display; on a HiDpi display the rendering now looks like this:

WM_DPICHANGED reducing size of the window

Setup:
Windows 10 Pro v1903
1st Display: 1920x1080 default DPI scaling
2nd Display: 1920x1080, 50% DPI scaling.
I am having problems implementing some DPI changes for Windows. The suggested rect (lParam of WM_DPICHANGED) is returning values of a smaller window once it gets to the scaled screen.
If I create a 1280x720 Window on 1st Display, then move it to the second display the suggested size becomes 1922x1044. The width makes sense since the resolution scaled should be 1920x1080. So I'm guessing it's factoring in the pixels for the borders. However, the height it is returning on the suggested rect, it smaller than what the Window should be if it was scaled properly. Now that it has shrunk to the new height if I go back to the original display, the new suggested size becomes 1280x694, we lose the original Window size.
I have tried manually setting the Window position to factor in a self calculated scale, (width * scale, height * scale) but it does not work properly since it does not factor borders and title bars correctly in SetWindowPos as I'm guessing it's expecting you to include those calculated sizes.
I have also tried reducing the window size to 1280x690 and moving it to the display 2 then back to display 1 and the size returns to the original size. So it's definitely related to the resolution of the window and the target display. I have tried testing other applications with DPI Scaling with a 1280x720 window (such as Microsoft Office) and they resize the window going to Display 2 and returning to Display 1 the size is the correct 1280x720 upon return, it does not experiencing the shrinking.
Am I missing something? I am using SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
Any help would be greatly appreciated.
but it does not work properly since it does not factor borders and
title bars correctly
You could try to use GetWindowRect to get the bounding rectangle of the window. And then scale the rectangle:
static float dpi;
BOOL InitInstance(...)
{
dpi = GetDpiForWindow(hWnd);
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DPICHANGED:
{
WORD newdpi = HIWORD(wParam); //The values of X-axis and the Y-axis are identical for Windows apps
float scale = newdpi / dpi;
dpi = newdpi;
//RECT* rect = (RECT*)lParam;
RECT old;
GetWindowRect(hWnd, &old);
SetWindowPos(hWnd,
NULL,
old.left,
old.top,
(old.right - old.left) * scale,
(old.bottom - old.top) * scale,
SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
return 0;
}

Are the coordinates in WINDOWPOS on WM_WINDOWPOSCHANGED in parent coordinates or screen coordinates?

Quick and simple one this time. I have a subclassed tab control that handles WM_WINDOWPOSCHANGED to, when not SWP_NOSIZE, resize its content. It passes the WINDOWPOS lParam's cx and cy fields to TCM_ADJUSTRECT to get the content rect size.
I need to do this on command as well (after changing tabs, say). However, I can't just do a dummy resize to the same size; even with an explicit SetWindowPos(), real Windows seems to add SWP_NOSIZE itself if the size doesn't change. So I want to write the analogous code to my WM_WINDOWPOSCHANGED handler.
My question is: what coordinate system are the coordinates given to WM_WINDOWPOSCHANGED given in, parent coordinates or screen coordinates? GetWindowRect() returns screen coordinates, so I'd like to know if I need to convert the coordinates to get the same values that I would get in WM_WINDOWPOSCHANGED. The documentation for WM_WINDOWPOSCHANGED doesn't say; neither does the documentation for WINDOWPOS.
Thanks.
WINDOWPOS, GetWindowRect, GetCursorPos, etc. give screen coordinates. When you use SetWindowPos you have to supply coordinates in relation to parent. This is straight forward for main window and popup windows which use screen coordinates. For moving child windows, you can use ScreenToClient and ClientToScreen for conversion.
For example, this will find the coordinates of OK button in relation to top-left corner of dialog box:
RECT rcOK;
HWND hitem = ::GetDlgItem(m_hWnd, IDOK);
GetWndRect(rcOK, hitem, m_hWnd);
void GetWndRect(RECT &rect, HWND item, HWND parent)
{
::GetWindowRect(item, &rect);//screen coordinates of OK button
POINT offset{ 0 };
ClientToScreen(parent, &offset); //Top-left (0,0) of client area of dialog -> screen coordinates
rect.left -= offset.x;
rect.right -= offset.x;
rect.top -= offset.y;
rect.bottom -= offset.y;
//client coordinates of OK button in relation to Dialog's Top-Right
}
Now we can move up the OK button by 10px:
rc.top -= 10;
rc.bottom -= 10;
::SetWindowPos(hitem, 0, rc.left, rc.top, 0, 0, SWP_NOSIZE);

How to get the title bar height and width for aero and basic design

I am using GetSystemMetrics(SM_CXSIZEFRAME) for the width of the border, it works with the basic design, but not with aero, how can I improve it, so it works with aero?
I am using GetSystemMetrics(SM_CYCAPTION) for the height of the title bar but the value is too small for both, basic and aero design, what am I doing wrong here?
In unthemed Windows, GetSystemMetrics(SM_CYCAPTION) is the height of the text in the title bar; you need to add in the size of the frame and border padding (GetSystemMetrics(SM_CYSIZEFRAME) + GetSystemMetrics(SM_CYEDGE) * 2).
For themed Windows (which is the default these days), GetThemeSysSize is most likely the function you're looking for; in particular, GetThemeSysSize(SM_CXBORDER) for the border width, and GetThemeSysSize(SM_CYSIZE) + GetThemeSysSize(SM_CXPADDEDBORDER) * 2 for the title bar.
I had a similar problem, where I needed to subtract the title bar height from the mouse Y to render a cursor in the window.
After looking around, and looking at functions like GetSystemMetrics, I ended up just using GetWindowRect and ClientToScreen.
I got the screen position of the client area using ClientToScreen with point 0,0.
Then subtracted that from the window top retrieved through GetWindowRect. Result is the distance from the top of the window to the inside of the window. Or the titlebar height.
int getWindowHeadSize()
{
RECT Rect;
GetWindowRect(hWnd, &Rect);
POINT point = { 0, 0 };
ClientToScreen(hWnd, &point);
return point.y - Rect.top + GetSystemMetrics(SM_CYSIZEFRAME) + GetSystemMetrics(SM_CYEDGE) * 2;
}

Why is font size different in vertical direction

I created two rulers - one vertical and one horizontal:
Now in the vertical ruler, is 'size' of the text visually larger(aprox. 5-6 pixels longer).
Why?
Relevant code:
WM_CREATE:
LOGFONT Lf = {0};
Lf.lfHeight = 12;
lstrcpyW(Lf.lfFaceName, L"Arial");
if (!g_pGRI->bHorizontal)
{
Lf.lfEscapement = 900; // <----For vertical ruler!
}
g_pGRI->hfRuler = CreateFontIndirectW(&Lf);
SelectFont(g_pGRI->hdRuler, g_pGRI->hfRuler);
WM_PAINT:
SetTextColor(g_pGRI->hdRuler, g_pGRI->cBorder);
SetBkColor(g_pGRI->hdRuler, g_pGRI->cBackground);
SetTextAlign(g_pGRI->hdRuler, TA_CENTER);
#define INCREMENT 10
WCHAR wText[16] = {0};
if (g_pGRI->bHorizontal)
{
INT ixTicks = RECTWIDTH(g_pGRI->rRuler) / INCREMENT;
for (INT ix = 0; ix < ixTicks + 1; ix++)
{
MoveToEx(g_pGRI->hdRuler, INCREMENT * ix, 0, NULL);
if (ix % INCREMENT == 0)
{
//This is major tick.
LineTo(g_pGRI->hdRuler, INCREMENT * ix, g_pGRI->lMajor);
wsprintfW(wText, L"%d", INCREMENT * ix);
TextOutW(g_pGRI->hdRuler, INCREMENT * ix + 1, g_pGRI->lMajor + 1, wText, CHARACTERCOUNT(wText));
}
else
{
//This is minor tick.
LineTo(g_pGRI->hdRuler, INCREMENT * ix, g_pGRI->lMinor);
}
}
}
else
{
INT iyTicks = RECTHEIGHT(g_pGRI->rRuler) / INCREMENT;
for (INT iy = 0; iy < iyTicks + 1; iy++)
{
MoveToEx(g_pGRI->hdRuler, 0, INCREMENT * iy, NULL);
if (iy % INCREMENT == 0)
{
//This is major tick.
LineTo(g_pGRI->hdRuler, g_pGRI->lMajor, INCREMENT * iy);
wsprintfW(wText, L"%d", INCREMENT * iy);
TextOutW(g_pGRI->hdRuler, g_pGRI->lMajor + 1, INCREMENT * iy + 1, wText, CHARACTERCOUNT(wText));
}
else
{
//This is minor tick.
LineTo(g_pGRI->hdRuler, g_pGRI->lMinor, INCREMENT * iy);
}
}
}
}
Background
There are several different schemes for rasterizing text in a legible way when the text is small relative to the size of a pixel. For example, if the stroke width is supposed to be 1.25 pixels wide, you either have to round it off to a whole number of pixels, use antialiasing, or use subpixel rendering (like ClearType). Rounding is usually controlled by "hints" built into the font by the font designer.
Hinting is the main reason why text width doesn't always scale exactly with the text height. For example, if, because of rounding, the left hump of a lowercase m is a pixel wider than the right one, a hint might tell the renderer to round the width up to make the letter symmetric. The result is that the character is a tad wider relative to its height than the ideal character.
This issue
What's likely happening here is that when GDI renders the string horizontally, each subsequent character may start at a fractional position, which is simulated by antialiasing or subpixel (ClearType) rendering. But, when rendering vertically, it appears that each subsequent character's starting position is rounded up to the next whole pixel, which tends to make the vertical text a couple pixels "longer" than its horizontal counterpart. Effectively, the kerning is always rounded up to the next whole pixel.
It's likely that more effort was put into the common case of horizontal text rendering, making it easier to read (and possibly faster to render). The general case of rendering at any other angle may have been implemented in a simpler manner, working glyph-by-glyph instead of with the entire string.
Things to Try
If you want them to look that same, you'll probably have to make a small compromise in the visual quality of the horizontal labels. Here are a few things I can think of to try:
Render the labels with regular antialiasing instead of ClearType subpixel rendering. (You can do this by setting the lfQuality field in the LOGFONT.) You would then draw the horizontal labels in the normal manner. For the vertical labels, draw them to an offscreen buffer horizontally, rotate it, and then blit the buffer to the screen. This gives you labels that look identical. The reason I suggest regular antialiasing is that it's invariant to the rotation. ClearType rendering had an inherent orientation and thus cannot be rotated without creating fringing. I've used this approach for graph labels with good results.
Render the horizontal labels character by character, rounding the starting point up to the next whole pixel. This should make the horizontal labels look like the vertical ones. Typographically, they won't look as good, but for small labels like this, it's probably less distracting than having the horizontal and vertical labels visually mismatched.
Another answer suggested rendering the horizontal labels with a very small, but non-zero, escapement and orientation, forcing those to go through the same rendering pipeline as the vertical labels. This may be the easiest solution for short labels like yours. If you had to handle longer strings of text, I'd suggest one of the first two methods.
When using lfEscapement, you will often get strange behaviour as it renders text using a fairly different pipeline.
A trick would be to have lfEscapement set for both. One with 900, and one with a very low value (such as 1 or even 10. Once you have both rendering with escapement, you should be good.
If you're still having issues with smoothing, try doing something like this:
BOOL bSmooth;
//Get previous smooth value.
SystemParametersInfo(SPI_GETFONTSMOOTHING, 0, &bSmooth, 0);
//Set no smoothing.
SystemParametersInfo(SPI_SETFONTSMOOTHING, 0, NULL, 0);
//Draw text.
//Return smoothing.
SystemParametersInfo(SPI_SETFONTSMOOTHING, bSmooth, NULL, 0);

Resources