winapi - rich edit control and vertical text layout (single line) - winapi

I have noticed that a text in the rich edit control (only a single line) is not centered vertically. A space between a text and a top border edge is larger than a space between a text and a botttom border edge. It is especially visible when a rich edit control height is only a little bit bigger that a text height. PARAMFORMAT only allow to set a horizontal alignment. How to set a vertical alignment / top-bottom margins ?
Edit:
This way I get PARAMFORMAT2 structure:
PARAFORMAT2 pf;
ZeroMemory(&pf, sizeof(pf));
pf.cbSize = sizeof(pf);
SendMessage(hwndRichEdit1, EM_GETPARAFORMAT, 0, (LPARAM)&pf);
dySpaceBefore is already initially set to 0 and the effect you can see on the attached screenshot.
I use Visual Studio 2017, MSFTEDIT_CLASS is defined in Richedit.h as L"RICHEDIT50W"

If you're using a Rich Edit 2.0 control, you can use the PARAFORMAT2 structure, which has the option to set the space before the text.
You haven't added a language tag, but here's how you would do it in C (see also the documentation for EM_SETPARAFORMAT):
//...
PARAFORMAT2 pf2;
pf2.cbSize = sizeof(PARAFORMAT2);
pf2.dwMask = PFM_SPACEBEFORE; // Of course, you can OR in other bits/options to set!
pf2.dySpaceBefore = 0; // Will align to the top; use a small +ve value, if you prefer
SendMessage(hWndEdit, EM_SETPARAFORMAT, 0, (LPARAM)&pf2);
//...
To get vertically centred text is bit more work, as you will need to get the height of the text (using GetTextExtent) and the height of the control's client rectangle, then use a 'space before' value of (client_height - text_height)/2.
Feel free to ask for further clarification and/or explanation. (I may even be able to offer you code in another language.)

I can reproduce this issue like this snapshot shows:
There seems no feature supported for vertical alignment center. I've submit a feature request internally.
A workaround is using EM_SETRECT which can move up text area via limiting rectangle into which the control draws the text. The following snapshots show its effects:
Then you can use it to adjust the text to display it in center between top and bottom.
Code example:
HWND hwndEdit = CreateWindowEx(
0,
MSFTEDIT_CLASS,
TEXT("EDIT"),
WS_BORDER | WS_VISIBLE | WS_CHILD,
20,
20,
100,
32,
hWnd,
NULL,
hInst,
NULL);
RECT rect;
SendMessage(hwndEdit, EM_GETRECT, 0, (LPARAM)&rect);
rect.top -= 2;
rect.bottom -= 2;
SendMessage(hwndEdit, EM_SETRECT, 1, (LPARAM)&rect);

Related

GetWindowRect returns a size including "invisible" borders

I'm working on an app that positions windows on the screen in a grid style. When Running this on Windows 10, there is a huge gap between the windows. Further investigation shows that GetWindowRect is returning unexpected values, including an invisible border, but I can't get it to return the real values with the visible border.
1) This thread suggests this is by design and you can "fix" it by linking with winver=6. My environment does not allow this but I've tried changing the PE MajorOperatingSystemVersion and MajorSubsystemVersion to 6 with no affect
2) That same thread also suggests using DwmGetWindowAttribute with DWMWA_EXTENDED_FRAME_BOUNDS to get the real coordinates from DWM, which works, but means changing everywhere that gets the window coordinates. It also doesn't allow the value to be set, leaving us to reverse the process to be able to set the window size.
3) This question suggests it's lack of the DPI awareness in the process. Neither setting the DPI awareness flag in the manifest, or calling SetProcessDpiAwareness had any result.
4) On a whim, I've also tried adding the Windows Vista, 7, 8, 8.1 and 10 compatibility flags, and the Windows themes manifest with no change.
This window is moved to 0x0, 1280x1024, supposedly to fill the entire screen, and when querying the coordinates back, we get the same values.
The window however is actually 14 pixels narrower, to take into account the border on older versions of Windows.
How can I convince Windows to let me work with the real window coordinates?
Windows 10 has thin invisible borders on left, right, and bottom, it is used to grip the mouse for resizing. The borders might look like this: 7,0,7,7 (left, top, right, bottom)
When you call SetWindowPos to put the window at this coordinates:
0, 0, 1280, 1024
The window will pick those exact coordinates, and GetWindowRect will return the same coordinates. But visually, the window appears to be here:
7, 0, 1273, 1017
You can fool the window and tell it to go here instead:
-7, 0, 1287, 1031
To do that, we get Windows 10 border thickness:
RECT rect, frame;
GetWindowRect(hwnd, &rect);
DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame, sizeof(RECT));
//rect should be `0, 0, 1280, 1024`
//frame should be `7, 0, 1273, 1017`
RECT border;
border.left = frame.left - rect.left;
border.top = frame.top - rect.top;
border.right = rect.right - frame.right;
border.bottom = rect.bottom - frame.bottom;
//border should be `7, 0, 7, 7`
Then offset the rectangle like so:
rect.left -= border.left;
rect.top -= border.top;
rect.right += border.left + border.right;
rect.bottom += border.top + border.bottom;
//new rect should be `-7, 0, 1287, 1031`
Unless there is a simpler solution!
How can I convince Windows to let me work with the real window coordinates?
You are already working with the real coordinates. Windows10 has simply chosen to hide the borders from your eyes. But nonetheless they are still there. Mousing past the edges of the window, your cursor will change to the resizing cursor, meaning that its still actually over the window.
If you want your eyes to match what Windows is telling you, you could try exposing those borders so that they are visible again, using the Aero Lite theme:
http://winaero.com/blog/enable-the-hidden-aero-lite-theme-in-windows-10/
AdjustWindowRectEx (or on Windows 10 and later AdjustWindowRectExForDpi) might be of use. These functions will convert a client rectangle into a window size.
I'm guessing you don't want to overlap the borders though, so this probably isn't a full solution--but it may be part of the solution and may be useful to other people coming across this question.
Here's a quick snippet from my codebase where I've successfully used these to set the window size to get a desired client size, pardon the error handling macros:
DWORD window_style = (DWORD)GetWindowLong(global_context->window, GWL_STYLE);
CHECK_CODE(window_style);
CHECK(window_style != WS_OVERLAPPED); // Required by AdjustWindowRectEx
DWORD window_style_ex = (DWORD)GetWindowLong(global_context->window, GWL_EXSTYLE);
CHECK_CODE(window_style_ex);
// XXX: Use DPI aware version?
RECT requested_size = {};
requested_size.right = width;
requested_size.bottom = height;
AdjustWindowRectEx(
&requested_size,
window_style,
false, // XXX: Why always false here?
window_style_ex
);
UINT set_window_pos_flags = SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER;
CHECK_CODE(SetWindowPos(
global_context->window,
nullptr,
0,
0,
requested_size.right - requested_size.left,
requested_size.bottom - requested_size.top,
set_window_pos_flags
));
There are still two ambiguities in the above use case:
My window does have a menu, but I have to pass in false for the menu param or I get the wrong size out. I'll update this answer with an explanation if I figure out why this is!
I haven't yet read about how Windows handles DPI awareness so I'm not sure when you want to use that function vs the non DPI aware one
You can respond to the WM_NCCALCSIZE message, modify WndProc's default behaviour to remove the invisible border.
As this document and this document explain, when wParam > 0, On request wParam.Rgrc[0] contains the new coordinates of the window and when the procedure returns, Response wParam.Rgrc[0] contains the coordinates of the new client rectangle.
The golang code sample:
case win.WM_NCCALCSIZE:
log.Println("----------------- WM_NCCALCSIZE:", wParam, lParam)
if wParam > 0 {
params := (*win.NCCALCSIZE_PARAMS)(unsafe.Pointer(lParam))
params.Rgrc[0].Top = params.Rgrc[2].Top
params.Rgrc[0].Left = params.Rgrc[0].Left + 1
params.Rgrc[0].Bottom = params.Rgrc[0].Bottom - 1
params.Rgrc[0].Right = params.Rgrc[0].Right - 1
return 0x0300
}

Win32: Editbox + Multiline + Wrap = Get Text Height?

I'm trying to stack some read-only textboxes on top of eachother to make a long scrollable list that can be cut and pasted.
So I create a simple textbox with a huge height....
HWND aTextBox=CreateWindow("Edit",theText.c(), WS_CHILD|WS_VISIBLE|ES_MULTILINE|ES_AUTOVSCROLL|ES_READONLY, 10, 10,aWidth-20,9999, aWindow, NULL, NULL, NULL);
SendMessage(aTextBox, WM_SETFONT, (WPARAM)gTextFont, 0);
...And now I want to figure out the height of the text within so that I can resize the textbox's height to fit.
I'm doing this right now:
HDC aHDC=GetDC(aTextBox);
RECT aRect;
DrawText(aHDC,theText.c(),theText.Len(),&aRect,DT_CALCRECT|DT_LEFT);
// Now size the textbox...
SetWindowPos(aTextBox,NULL,0,0,aWidth-20,aRect.bottom-aRect.top,SWP_NOMOVE);
I am getting strange results that seem to be close... but not exact. Some are too large, some are too small, some are spot on.
Anyone know what I'm doing wrong here?

Win32 tooltip appears on the top of the screen when it goes out on the bottom

I am using my own tooltip to display quick help about a currently selected item in my autocomplete listbox used in my editor. Just like VS does it for source code editor, when a new selection happens, the tooltip permanently and immediately pops up next to the current selection, and remains there until new selection of autocomplete listbox is gone.
This is a WTL project if that matters.
The way I create amd display my tooltip:
m_hwndTooltip = CreateWindowEx(WS_EX_TOPMOST,
TOOLTIPS_CLASS,
NULL,
TTS_NOPREFIX | TTS_ALWAYSTIP,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
NULL,
NULL
);
// INITIALIZE MEMBERS OF THE TOOLINFO STRUCTURE
m_toolTipInfo.cbSize = TTTOOLINFOA_V2_SIZE;
m_toolTipInfo.uFlags = TTF_TRACK;
m_toolTipInfo.hwnd = NULL;
m_toolTipInfo.hinst = NULL;
m_toolTipInfo.uId = 0; // ??
m_toolTipInfo.lpszText = (LPTSTR) messsssssssage.c_str();
// Tool_tip control will cover the whole window
m_toolTipInfo.rect.left = 0;
m_toolTipInfo.rect.top = 0;
m_toolTipInfo.rect.right = 0;
m_toolTipInfo.rect.bottom = 0;
::SendMessage(m_hwndTooltip, TTM_SETMAXTIPWIDTH, 0, MAX_TOOLTIP_WIDTH); //set max width in pixels, AND(!!) enable multi-line support
// SEND AN ADDTOOL MESSAGE TO THE TOOLTIP CONTROL WINDOW
::SendMessage(m_hwndTooltip, TTM_ADDTOOL, 0, (LPARAM) (LPTOOLINFO) &m_toolTipInfo);
::SendMessage(m_hwndTooltip, TTM_TRACKPOSITION, 0, (LPARAM)(DWORD) MAKELONG(x, y));
::SendMessage(m_hwndTooltip, TTM_TRACKACTIVATE, true, (LPARAM)(LPTOOLINFO) &m_toolTipInfo);
This is all fine, works like a charm.
However, when I pass too high Y coord which would make tooltip out of the screen on the bottom side (eg: Screen height: 1000, and I pass Y: 950, and tooltip will be 100 height), then the Tooltip appears on the Y=0, so the screen top position, instead of repositioning to 900.
However, this works horizontally, so if it would be out on the right side (so too high X is passed), it moves left the Tooltip until it can fit on the screen.
This is very strange and weird?!
Can anyone give me a tip what is the problem here?
Not to mention, the Tooltip size is determined by the win tooltip automatically, based on the message to display + max width + font used + line numbers, so I cannot calculate a correct X,Y position before displaying it, so I need to rely on the Tooltip control.
As no reply, and I found the solution, I will share it for others:
It seems the initially 0,0 is a kind of fallback when tooltip fails to fit on the screen.
So what I do, instead of
m_toolTipInfo.rect.top = 0;
m_toolTipInfo.rect.buttom = 0;
--->
m_toolTipInfo.rect.top = rcWorkingArea.bottom - 5;
m_toolTipInfo.rect.bottom = rcWorkingArea.bottom - 5;
If tooltip cannot fit on the screen bottom side, this value will be used.
Still strange why does it work automatically with X coord...but never mind.

Flicker/dead region issues maximizing an MFC window

I'm trying to make an MFC window (a CDialog) go fullscreen whenever the user attempts to maximize it. The window is being used as an OpenGL context. I'm attempting to do everything inside of the CDialog::OnSize callback. Here's the code that I'm using:
void MyCDialogSubclass::OnSize(UINT action, int width, int height) {
CDialog::OnSize(action, width, height);
switch (action) {
case SIZE_MAXIMIZED:
if (GetStyle() & WS_OVERLAPPEDWINDOW) {
MONITORINFO screen;
screen.cbSize = sizeof(screen);
if (GetMonitorInfo(MonitorFromWindow(GetSafeHwnd(), MONITOR_DEFAULTTOPRIMARY), &screen)) {
ModifyStyle(WS_OVERLAPPEDWINDOW, 0, 0);
width = screen.rcMonitor.right - screen.rcMonitor.left;
height = screen.rcMonitor.bottom - screen.rcMonitor.top;
SetWindowPos(&wndTop, screen.rcMonitor.left, screen.rcMonitor.top, width, height, SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
}
}
break;
case SIZE_MINIMIZED:
case SIZE_RESTORED:
if (!(GetStyle() & WS_OVERLAPPEDWINDOW)) {
ModifyStyle(0, WS_OVERLAPPEDWINDOW, 0);
SetWindowPos(NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
}
break;
}
if (wglMakeCurrent(my_hdc, my_hglrc))
my_opengl_reshape_call(width, height);
wglMakeCurrent(NULL, NULL);
}
If I comment out the ModifyStyle() calls, everything works fine, with the obvious proviso that the window style stays normal, so there's a standard window title bar across the top of the screen that I want to get rid of. If I keep the ModifyStyle() calls and comment out the SetWindowPos() calls, the title bar and everything else disappears, but the window has a black region along the top of the screen that is the exact height of the title bar—as though it is being reserved. If I don't comment out either of the pairs of calls, as shown in the code above, the screen flickers violently. I believe it's flickering back and forth between the black region being present and not being present, but it's difficult to tell. This flickering also appears to corrupt video memory, as I get persistent artifacts in window title bars (in different applications, no less) and, once, the login picture in the Start menu was replaced with one of my OpenGL textures.
The code that I'm using is adapted from the code that Stefan linked in an answer below, from The Old New Thing, which is working better than my original code. I'm assuming this problem isn't arising from my decision not to insert code to save the window placement (per The Old New Thing), because this happens before I ever try to restore the window.
Don't maximize the window if you want it to be full screen.
Use this approach instead.

Variable item height in TreeView gives broken lines

Wee.
So I finally figured out how the iIntegral member of TVITEMEX works. The MSDN docs didn't think to mention that setting it while inserting an item has no effect, but setting it after the item is inserted works. Yay!
However, when using the TVS_HASLINES style with items of variable height, the lines are only drawn for the top part of an item with iIntegral > 1. E.g. if I set TVS_HASLINES and TVS
Here's what it looks like (can't post images WTF?)
Should I manually draw more of the lines in response to NM_CUSTOMDRAW or something?
Yes, Windows doesn't do anything with the blank space obtained from changing the height.
From the MSDN:
The tree-view control does not draw in the
extra area, which appears below the
item content, but this space can be
used by the application for drawing
when using custom draw. Applications
that are not using custom draw should
set this value to 1, as otherwise the
behavior is undefined.
Alright, problem solved.
I failed to find an easy answer, but I did work around it the hard way. It's basically just drawing the extra line segments in custom draw:
// _cd is the NMTVCUSTOMDRAW structure
// ITEMHEIGHT is the fixed height set in TreeView_SetItemHeight
// linePen is HPEN of a suitable pen to draw the lines (PS_ALTERNATE etc.)
// indent is the indentation size returned from TreeView_GetIndent
case CDDS_ITEMPREPAINT : {
// Expand line because TreeView is buggy
RECT r = _cd->nmcd.rc;
HDC hdc = _cd->nmcd.hdc;
HTREEITEM hItem = (HTREEITEM) _cd->nmcd.dwItemSpec;
if( r.bottom - r.top > ITEMHEIGHT ) {
HGDIOBJ oldPen = SelectObject( hdc, linePen );
// Draw any lines left of current item
HTREEITEM hItemScan = hItem;
for( int i = _cd->iLevel; i >= 0; --i ) {
// Line should be drawn only if node has a next sibling to connect to
if( TreeView_GetNextSibling( getHWnd(), hItemScan ) ) {
// Lines seem to start 17 pixels from left edge of control. But no idea
// where that constant comes from or if it is really constant.
int x = 17 + indent * i;
MoveToEx( hdc, x, r.top + ITEMHEIGHT, 0 );
LineTo( hdc, x, r.bottom );
}
// Do the same for the parent
hItemScan = TreeView_GetParent( getHWnd(), hItemScan );
}
SelectObject( hdc, oldPen );
}
}
The pattern from the PS_ALTERNATE brush sometimes doesn't align perfectly with line drawn by the control, but that's hardly noticeable. What's worse is that even though I have the latest common controls and all the service packs and hotfixes installed, there are still bugs in TreeView documented way back in 2005. Specifically, the TreeView doesn't update its height correctly. The only workaround I've found for that is to force some collapsing/expanding of nodes and do a few calls to InvalidateRect.
If the variable-height nodes are at the root level, though, there doesn't appear to be anything you can do. Luckily I don't need that.

Resources