How to use font without ligatures via WinAPI? - c++

I'm trying to create simple application with custom output. I'm using CreateFont function in order to load monospaced font with specific options. As a result, the text has been drawn (DrawText) with ligatures such as fi.
How to disable this?
I am very sorry for my bad English
Creating font
normal = CreateFont(char_height, 0, 0, 0, FW_NORMAL, 0, 0, 0,
RUSSIAN_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, 5,
FIXED_PITCH | FF_DONTCARE, "Menlo");
bold = CreateFont(char_height, 0, 0, 0, FW_BOLD, 0, 0, 0,
RUSSIAN_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, 5,
FIXED_PITCH | FF_DONTCARE, "Menlo");
Printing
void Console::Print(string text, int color, int weight) {
RECT r;
r.top = padding + temp_y * (char_height + space_y);
r.bottom = r.top + char_height;
r.left = padding + temp_x * char_width;
r.right = padding + length();
HDC hdc = GetDC(hwnd);
SetTextColor(hdc, color);
SetBkColor(hdc, RGB(43, 48, 59));
SelectObject(GetDC(hwnd), weight == WEIGHT_NORMAL ? normal : bold);
DrawText(GetDC(hwnd), text.c_str(), text.length(), &r, DT_LEFT);
temp_x += text.length();
}
Current output (note the word "offline"):
Desired output:

The root problem seems to be that your font is broken. I'm not sure why a monospace (fixed-pitch) font would have ligature information.
Solution 1
Use ExtTextOutW with ETO_IGNORELANGUAGE. You may lose other functionality (like bi-di, digit substitution, shaping for complex scripts), but it does seem to prevent kerning and ligation, so it may be suitable for your purpose.
::ExtTextOutW(hdc, x, y, ETO_IGNORELANGUAGE, &rc, msg.c_str(), msg.size(), nullptr);
I tested with the font Gabriola, since I don't have Menlo or Meslo. Gabriola is a variable pitch font, but it has distinctive ligatures that make it easy to spot, especially fi.
Solution 2
A second approach, which has the same drawbacks, is to draw the strings character by character with TextOut in a loop. This is a little trickier because you have to worry about Unicode surrogate pairs, clipping, and updating the current position.
const auto old_alignment = ::SetTextAlign(hdc, TA_UPDATECP);
const auto old_mode = ::SetBkMode(hdc, TRANSPARENT);
::MoveToEx(hdc, x, y, nullptr);
// Loop simplified for demo. This doesn't handle Unicode surrogate pairs.
for (auto ch : msg) {
::TextOutW(hdc, 0, 0, &ch, 1);
}
::SetBkMode(hdc, old_mode);
::SetTextAlign(hdc, old_alignment);
This produced identical results to the first solution.
Non-Solution
Note that my earlier idea to use GetCharacterPlacement without the GCP_LIGATE flag followed by ExtTextOut with ETO_GLYPH_INDEX does not work. The glyphs returned from GetCharacterPlacement still included ligatures even without the GCP_LIGATE flag.
// DOES NOT WORK
GCP_RESULTSW results = {sizeof(results)};
WCHAR modified[64] = L""; // FIXED BUFFER LENGTHS JUST FOR TESTING
results.lpOutString = modified;
int deltas[64] = {0};
results.lpDx = deltas;
WCHAR glyphs[64] = L"";
glyphs[0] = 1;
results.lpGlyphs = glyphs;
results.nGlyphs = ARRAYSIZE(glyphs);
const DWORD flags = GCP_REORDER; // but not GCP_LIGATE or GCP_USEKERNING
::GetCharacterPlacementW(hdc, msg.c_str(), msg.size(), 0, &results, flags);
::ExtTextOutW(hdc, x, y, ETO_GLYPH_INDEX, &rc, glyphs, results.nGlyphs, deltas);

Related

How to draw text with transparency using GDI?

My goal is to dynamically put some arbitrary text into an HICON image (at runtime.) I'm using the following code:
//Error checks are omitted for brevity
//First create font
LOGFONT lf = {0};
lf.lfHeight = -58;
lf.lfWeight = FW_NORMAL;
lf.lfOutPrecision = OUT_TT_PRECIS; //Use TrueType fonts for anti-alliasing
lf.lfQuality = CLEARTYPE_QUALITY;
lstrcpy(lf.lfFaceName, L"Segoe UI");
HFONT hFont = ::CreateFontIndirect(&lf);
//HICON hIcon = original icon to use as a source
//I'm using a large 256x256 pixel icon
hIcon = (HICON)::LoadImage(theApp.m_hInstance, MAKEINTRESOURCE(IDI_ICON_GREEN_DIAMOND), IMAGE_ICON, 256, 256, LR_DEFAULTCOLOR);
ICONINFO ii = {0};
::GetIconInfo(hIcon, &ii);
BITMAP bm = {0};
::GetObject(ii.hbmColor, sizeof(bm), &bm);
SIZE szBmp = {bm.bmWidth, bm.bmHeight};
HDC hDc = ::GetDC(hWnd);
HDC hMemDC = ::CreateCompatibleDC(hDc);
HGDIOBJ hOldBmp = ::SelectObject(hMemDC, ii.hbmColor);
HGDIOBJ hOldFont = ::SelectObject(hMemDC, hFont);
::SetBkMode(hMemDC, TRANSPARENT);
::SetTextColor(hMemDC, RGB(255, 0, 0)); //Red text
//Draw text
//NOTE that DrawText API behaves in a similar way
::TextOut(hMemDC, 0, 0, L"Hello", 5);
::SelectObject(hMemDC, hOldFont);
::SelectObject(hMemDC, hOldBmp);
//We need a simple mask bitmap for the icon
HBITMAP hBmpMsk = ::CreateBitmap(szBmp.cx, szBmp.cy, 1, 1, NULL);
ICONINFO ii2 = {0};
ii2.fIcon = TRUE;
ii2.hbmColor = ii.hbmColor;
ii2.hbmMask = hBmpMsk;
//Create updated icon
HICON hIcon2 = ::CreateIconIndirect(&ii2);
//Cleanup
::DeleteObject(hBmpMsk);
::DeleteDC(hMemDC);
::ReleaseDC(hWnd, hDc);
::DeleteObject(ii.hbmColor);
::DeleteObject(ii.hbmMask);
::DeleteObject(hFont);
and then I can display the icon in my window from OnPaint() handler (so that I can see how it turns out) as such:
::DrawIconEx(dc.GetSafeHdc(), 0, 0,
hIcon2,
256, 256, NULL,
::GetSysColorBrush(COLOR_BTNFACE),
DI_NORMAL);
So here's what I get:
To see what's going on pixel-wise in my hIcon2 I called GetDIBits on its ii.hbmColor from the code above. The resulting pixel array where my word "Hello" was supposed to be shown looked like this:
The pixels are encoded as BGRA in that memory dump, so the 4th byte in each DWORD stands for transparency: 0=transparent, FF=opaque. But in this case TextOut doesn't fill out transparency, or leaves it as 0, which is interpreted as "fully transparent." Instead it seems to pre-multiply it into the RGB colors themselves.
Note that if I keep looking further down the same bitmap, where the green diamond begins, the image pixels seem to have transparency bytes set correctly:
Any idea how to draw text so that the API could set those transparency bytes?
EDIT: As was suggested below I tried the following GDI+ method:
HGDIOBJ hOldBmp = ::SelectObject(hMemDC, ii.hbmColor);
Graphics grpx(hMemDC);
RectF rcfTxt(0.0f, 0.0f, (REAL)szBmp.cx, (REAL)szBmp.cy);
Font gdiFont(L"Segoe UI", 58.0f, FontStyleRegular, UnitPixel);
SolidBrush gdiBrush(Color(255, 0, 0));
StringFormat gdiSF;
gdiSF.SetAlignment(StringAlignmentNear);
gdiSF.SetFormatFlags(StringFormatFlagsNoWrap);
gdiSF.SetHotkeyPrefix(HotkeyPrefixNone);
//The reason I was using GDI was because I was setting
//spacing between letters using SetTextCharacterExtra()
//Unfortunately with GDI+ this does not work!
HDC hTmpDC = grpx.GetHDC();
::SetTextCharacterExtra(hTmpDC, -4); //This doesn't do anything!
grpx.ReleaseHDC(hTmpDC);
grpx.DrawString(L"Hello", 5, &gdiFont, rcfTxt, &gdiSF, &gdiBrush);
::SelectObject(hMemDC, hOldBmp);
and besides not being able to set character spacing (which I could with GDI using SetTextCharacterExtra) here's what I got (slightly enlarged for visibility):
So clearly still an issue with transparency.
Taken from an old post by Microsoft MVP Mike D Sutton here.
When you create a DC it initially has default 'stock' objects selected
into it, including the stock 1*1*1 Bitmap. Since there is a Bitmap
already selected into the DC when you call DrawText() it will still
try and render to it even though pretty much everything (apart from
one pixel) will be clipped.
What you need to do is to create a Bitmap,
either DDB or DIBSection, and select that into your DC before drawing
to it.
First though you need to find the size of your Bitmap since you
want it large enough to display your text in, so for that you use the
DrawText() call again on the initial DC but include the DT_CALCRECT
flag. What this does is rather than drawing anything it simply
measures how large the text is and dumps that into the RECT you pass
the call. From here you can go ahead and create your DIBSection using
those dimensions and select it into your DC. Finally perform your
existing DrawText ()call (you may also want to use SetBkMode/Color())
which will render the text to the DIBSection from which you can get at
the data.
This seems to work pretty well here:
HBITMAP CreateAlphaTextBitmap(LPCSTR inText, HFONT inFont, COLORREF inColour) {
int TextLength = (int)strlen(inText);
if (TextLength <= 0) return NULL;
// Create DC and select font into it
HDC hTextDC = CreateCompatibleDC(NULL);
HFONT hOldFont = (HFONT)SelectObject(hTextDC, inFont);
HBITMAP hMyDIB = NULL;
// Get text area
RECT TextArea = {0, 0, 0, 0};
DrawText(hTextDC, inText, TextLength, &TextArea, DT_CALCRECT);
if ((TextArea.right > TextArea.left) && (TextArea.bottom > TextArea.top)) {
BITMAPINFOHEADER BMIH;
memset(&BMIH, 0x0, sizeof(BITMAPINFOHEADER));
void *pvBits = NULL;
// Specify DIB setup
BMIH.biSize = sizeof(BMIH);
BMIH.biWidth = TextArea.right - TextArea.left;
BMIH.biHeight = TextArea.bottom - TextArea.top;
BMIH.biPlanes = 1;
BMIH.biBitCount = 32;
BMIH.biCompression = BI_RGB;
// Create and select DIB into DC
hMyDIB = CreateDIBSection(hTextDC, (LPBITMAPINFO)&BMIH, 0, (LPVOID*)&pvBits, NULL, 0);
HBITMAP hOldBMP = (HBITMAP)SelectObject(hTextDC, hMyDIB);
if (hOldBMP != NULL) {
// Set up DC properties
SetTextColor(hTextDC, 0x00FFFFFF);
SetBkColor(hTextDC, 0x00000000);
SetBkMode(hTextDC, OPAQUE);
// Draw text to buffer
DrawText(hTextDC, inText, TextLength, &TextArea, DT_NOCLIP);
BYTE* DataPtr = (BYTE*)pvBits;
BYTE FillR = GetRValue(inColour);
BYTE FillG = GetGValue(inColour);
BYTE FillB = GetBValue(inColour);
BYTE ThisA;
for (int LoopY = 0; LoopY < BMIH.biHeight; LoopY++) {
for (int LoopX = 0; LoopX < BMIH.biWidth; LoopX++) {
ThisA = *DataPtr; // Move alpha and pre-multiply with RGB
*DataPtr++ = (FillB * ThisA) >> 8;
*DataPtr++ = (FillG * ThisA) >> 8;
*DataPtr++ = (FillR * ThisA) >> 8;
*DataPtr++ = ThisA; // Set Alpha
}
}
// De-select bitmap
SelectObject(hTextDC, hOldBMP);
}
}
// De-select font and destroy temp DC
SelectObject(hTextDC, hOldFont);
DeleteDC(hTextDC);
// Return DIBSection
return hMyDIB;
}
If you need an example of how to call it then try something like this
(inDC is the DC to render to):
void TestAlphaText(HDC inDC, int inX, int inY) {
const char *DemoText = "Hello World!\0";
RECT TextArea = {0, 0, 0, 0};
HFONT TempFont = CreateFont(50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "Arial\0");
HBITMAP MyBMP = CreateAlphaTextBitmap(DemoText, TempFont, 0xFF);
DeleteObject(TempFont);
if (MyBMP) { // Create temporary DC and select new Bitmap into it
HDC hTempDC = CreateCompatibleDC(inDC);
HBITMAP hOldBMP = (HBITMAP)SelectObject(hTempDC, MyBMP);
if (hOldBMP) {
BITMAP BMInf; // Get Bitmap image size
GetObject(MyBMP, sizeof(BITMAP), &BMInf);
// Fill blend function and blend new text to window
BLENDFUNCTION bf;
bf.BlendOp = AC_SRC_OVER;
bf.BlendFlags = 0;
bf.SourceConstantAlpha = 0x80;
bf.AlphaFormat = AC_SRC_ALPHA;
AlphaBlend(inDC, inX, inY, BMInf.bmWidth, BMInf.bmHeight,
hTempDC, 0, 0, BMInf.bmWidth, BMInf.bmHeight, bf);
// Clean up
SelectObject(hTempDC, hOldBMP);
DeleteObject(MyBMP);
DeleteDC(hTempDC);
}
}
}
All credit to answer and code go to original posters on that forum, I've simply reposted it so that this answer will be valid if the links die.
This reply is coming almost 3 years after the question was posted, but people still consult these things long into the future. So I'll explain what's happening.
DrawText (and other GDI text functions) will work on a transparent bitmap. The text is not coming out black even though it displays that way. The alpha channel is set to 0 on all pixels the text draws to, overriding whatever alpha you had set previously. If you set an alpha value in SetTextColor the text will render all black. If you're feeling ambitious you can run through pixel by pixel and target anything not your fill color (which requires a single fill color) but the problem then becomes one of the nature of ClearType being overridden and all alphas are set to whatever you set them to. The text ends up looking very funky. If you use a constant alpha for your background fill you can simply do a blanket run across the entire bitmap's bits after the text is drawn and reset all the alpha values. Since you have to read a byte to determine if it's background or not, you might as well just set every pixel's alpha to whatever the standard alpha is for that image and bypass the slow compares. This works reasonably well and I've found it to be very acceptable. In this day and age, MS should have taken care of this long ago but it's not to be.
https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-antialiasing-with-text-use
Gdiplus::Bitmap bmp( your_Width, your_Height, PixelFormat64bppARGB);
//PixelFormat64bppARGB ARGB needed
FontFamily fontFamily(L"Arial");
Font font(&fontFamily, 29, FontStyleRegular, UnitPoint);
Gdiplus::RectF rectF(00.0f, 10.0f, your_Width, your_Height);
StringFormat stringFormat;
SolidBrush solidBrush(Color(63, 0, 0, 255));
stringFormat.SetAlignment(StringAlignmentCenter);
//solidBrush Color(63, 0, 0, 255) ARGB neede
graphics.SetTextRenderingHint(TextRenderingHintAntiAlias);
graphics.DrawString("your_text", -1, &font, rectF, &stringFormat, &solidBrush);
//TextRenderingHintAntiAlias this needed

Replicate DOS console font (CP437) in Win32 app

I am converting an old Console app to Win32, and want to replicate the font from the console. The existing codebase forces me to work in C/C++. I'm trying to use CreateFont and CreateFontIndirect to construct an equivalent.
The console font settings are:
I think I understand that raster fonts are not TTF and not directly supported, thanks to this post, How to use DOS font in WinForms application, and arbiter's answer.
I want to construct a fixed font that matches the 12 pixel height and 8 pixel width.
Here's some of the code I've tried so far.
HDC hdc = GetDC(w_child);
// "A 12-point font is 16 pixels tall." -- https://msdn.microsoft.com/en-us/library/windows/desktop/ff684173(v=vs.85).aspx
// "An n-point font is 4/3*n pixels tall"?
// I want 12 pixels tall, so 9-point, right?
int PointSize = 9;
int nHeight = -MulDiv(PointSize, GetDeviceCaps(hdc, LOGPIXELSY), 72);
ReleaseDC(w_child, hdc);
HFONT hf = CreateFont(
-12, //nHeight, // Logical height
0, //nHeight * 2/3, // Logical avg character width
0, // Angle of escapement (0)
0, // Baseline angle (0)
FW_DONTCARE, // Weight (0)
FALSE, // Italic (0)
FALSE, // Underline (0)
FALSE, // Strikeout (0)
ANSI_CHARSET, // Character set identifier ??
OUT_DEFAULT_PRECIS, // Output precision
CLIP_DEFAULT_PRECIS, // Clip precision (0)
DEFAULT_QUALITY, // Output quality
FIXED_PITCH, // Pitch and family
"Lucida Console" // Pointer to typeface name string
//"Terminal"
//"Courier New"
);
*/
// Getting stock font, creating an indirect as a logical modification,
// seems to work better.
// ANSI_FIXED_FONT, with lf.lfHeight = 10, results in something clear and
// readable, but a little too large.
// And changing lfHeight seems to have no impact.
HFONT hf = (HFONT)GetStockObject(ANSI_FIXED_FONT);
// SYSTEM_FIXED_FONT at lfHeight = 10 is way too big
HFONT hf = (HFONT)GetStockObject(SYSTEM_FIXED_FONT);
// DEVICE_DEFAULT_FONT at lfHeight = 8 is way too big.
HFONT hf = (HFONT)GetStockObject(DEVICE_DEFAULT_FONT);
LOGFONT lf;
GetObject(hf, sizeof(LOGFONT), &lf);
lf.lfHeight = 12;
HFONT nf = CreateFontIndirect(&lf);
SendMessage(w_child, WM_SETFONT, (WPARAM)nf, TRUE);
Seems like success in my situation:
CreateFont(12, 8, 0, 0, 0, FALSE, 0, 0, OEM_CHARSET, OUT_RASTER_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, FIXED_PITCH, L"System");

How to resize a Win32 listbox to fit its content?

Is there any way to resize a Win32 listbox to fit its content (the minimum size that will show all its content, not needing a scrollbar), whenever its items change?
Thank!
Edit: I need resize both width and height of listbox.
You didn't specify whether you wanted horizontal as well as vertical, but I'm going to assume not. Basically, you need to get the number of items and the item height and multiply them, then add on the space for the control borders (unless the control is borderless, you may need to play around with this):
void AutosizeListBox(HWND hWndLB)
{
int iItemHeight = SendMessage(hWndLB, LB_GETITEMHEIGHT, 0, 0);
int iItemCount = SendMessage(hWndLB, LB_GETCOUNT, 0, 0);
// calculate new desired client size
RECT rc;
GetClientRect(hWndLB, &rc);
rc.bottom = rc.top + iItemHeight * iItemCount;
// grow for borders
rc.right += GetSystemMetrics(SM_CXEDGE) * 2;
rc.bottom += GetSystemMetrics(SM_CXEDGE) * 2;
// resize
SetWindowPos(hWndLB, 0, 0, 0, rc.right, rc.bottom, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}
If you want horizontal sizing as well you would need to select the right font into a DC, and loop through all the items to calculate the maximum text length using GetTextExtentPoint32.
EDIT: Added a version that calculates horizontal size as well.
void AutosizeListBox(HWND hWndLB)
{
int iItemHeight = SendMessage(hWndLB, LB_GETITEMHEIGHT, 0, 0);
int iItemCount = SendMessage(hWndLB, LB_GETCOUNT, 0, 0);
// get a DC and set up the font
HDC hDC = GetDC(hWndLB);
HGDIOBJ hOldFont = SelectObject(hDC, (HGDIOBJ)SendMessage(hWndLB, WM_GETFONT, 0, 0));
// calculate width of largest string
int iItemWidth = 0;
for (int i = 0; i < iItemCount; i++)
{
int iLen = SendMessage(hWndLB, LB_GETTEXTLEN, i, 0);
TCHAR* pBuf = new TCHAR[iLen + 1];
SendMessage(hWndLB, LB_GETTEXT, i, (LPARAM)pBuf);
SIZE sz;
GetTextExtentPoint32(hDC, pBuf, iLen, &sz);
if (iItemWidth < sz.cx) iItemWidth = sz.cx;
delete[] pBuf;
}
SelectObject(hDC, hOldFont);
ReleaseDC(hWndLB, hDC);
// calculate new desired client size
RECT rc;
SetRect(&rc, 0, 0, iItemWidth, iItemHeight * iItemCount);
// grow for borders
rc.right += GetSystemMetrics(SM_CXEDGE) * 2;
rc.bottom += GetSystemMetrics(SM_CXEDGE) * 2;
// resize
SetWindowPos(hWndLB, 0, 0, 0, rc.right, rc.bottom, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}

Calculate ideal font size, based on the paper size and maximum allowed text length

I have printing code that draws grid on the paper.
Grid has 4 columns, and they have equal horizontal length. Height of the cell is tenth of the paper size. Total number of rows is unknown but I know for a fact that there will be at least one row.
Each cell has same physical size-> width is quarter of the paper width, and height is one tenth of the paper height. Maximum number of characters that can fit into cell is 50.
The problem I face is choosing proper font size so text of maximum length can fit into cell.
Browsing through MSDN documentation and WinAPI examples, I saw that they use GetTextExtPoint32 for similar purposes, but this works only if font already exists and is selected into device context, which is not the case here.
The only thing that crossed my mind was to create "dummy font", see if the example text can fit into cell, and then adjust it's size if the test fails. I have also found this blog that recommends interesting approach to this problem, but being inexperienced I can't decide if "this is the proper way to go".
Can you recommend a correct solution for my problem?
EDITED on June, 30th 2014:
Below is the sample function that draws grid and paints upper left cell in light gray since that cell will contain sample text. That way we can visually validate the success of our drawing code:
// hWnd is the window that owns the property sheet.
HRESULT GDI_PRINT(HWND hWnd)
{
HRESULT hResult;
PRINTDLGEX pdx = {0};
LPPRINTPAGERANGE pPageRanges = NULL;
// Allocate an array of PRINTPAGERANGE structures.
pPageRanges = (LPPRINTPAGERANGE) GlobalAlloc(GPTR, 10 * sizeof(PRINTPAGERANGE));
if (!pPageRanges)
return E_OUTOFMEMORY;
// Initialize the PRINTDLGEX structure.
pdx.lStructSize = sizeof(PRINTDLGEX);
pdx.hwndOwner = hWnd;
pdx.hDevMode = NULL;
pdx.hDevNames = NULL;
pdx.hDC = NULL;
pdx.Flags = PD_RETURNDC;
pdx.Flags2 = 0;
pdx.ExclusionFlags = 0;
pdx.nPageRanges = 0;
pdx.nMaxPageRanges = 10;
pdx.lpPageRanges = pPageRanges;
pdx.nMinPage = 1;
pdx.nMaxPage = 1000;
pdx.nCopies = 1;
pdx.hInstance = 0;
pdx.lpPrintTemplateName = NULL;
pdx.lpCallback = NULL;
pdx.nPropertyPages = 0;
pdx.lphPropertyPages = NULL;
pdx.nStartPage = START_PAGE_GENERAL;
pdx.dwResultAction = 0;
// Invoke the Print property sheet.
hResult = PrintDlgEx(&pdx);
if ( ( hResult == S_OK ) && ( pdx.dwResultAction == PD_RESULT_PRINT ) )
{
// User clicked the Print button,
// so use the DC and other information returned in the
// PRINTDLGEX structure to print the document.
//======= Various initializations ==========//
DOCINFO diDocInfo = {0};
diDocInfo.cbSize = sizeof( DOCINFO );
diDocInfo.lpszDocName = L"Testing printing...";
int pageWidth = GetDeviceCaps( pdx.hDC, HORZRES ),
pageHeight = GetDeviceCaps( pdx.hDC, VERTRES );
//===================== IMPORTANT !!! ==========================//
// Must test this on real printer !!! //
// For now testing is done in XPS and MS OneNote2007 //
//==============================================================//
//================== end of initialization =====================//
if( StartDoc( pdx.hDC, &diDocInfo ) > 0 )
{
if( StartPage( pdx.hDC ) > 0 )
{
//===== creating red pen that will draw grid =====//
LOGBRUSH lb;
lb.lbColor = RGB( 255, 0, 0 );
lb.lbHatch = 0;
lb.lbStyle = BS_SOLID;
HPEN hPen = ExtCreatePen( PS_COSMETIC | PS_SOLID, 1, &lb, 0, NULL);
HGDIOBJ oldPen = SelectObject( pdx.hDC, hPen );
// create test font
HFONT font, oldFont;
long lfHeight = -MulDiv( 14,
GetDeviceCaps( pdx.hDC, LOGPIXELSY ),
72 );
font = CreateFont( lfHeight, 0, 0, 0,
FW_BOLD, TRUE, FALSE, FALSE,
0, 0, 0,
0, 0, L"Microsoft Sans Serif" );
oldFont = SelectFont( pdx.hDC, font );
SetBkMode( pdx.hDC, TRANSPARENT );
SetTextColor( pdx.hDC, RGB( 255, 0, 0 ) );
// testing rectangle -> top left cell of the grid
RECT rcText;
rcText.left = 0;
rcText.top = 0;
rcText.right = pageWidth / 4;
rcText.bottom = pageHeight / 10;
// fill destination rectangle with gray brush
// so we can visually validate rectangle coordinates
FillRect( pdx.hDC, &rcText, (HBRUSH)GetStockObject(LTGRAY_BRUSH) );
// implement solution mentioned in the comment to this question
SIZE s;
::GetTextExtentPoint32( pdx.hDC,
L"Хидрогеотермална енергија Хидрогеотермална енерги",
wcslen( L"Хидрогеотермална енергија Хидрогеотермална енерги" ),
&s );
// select old font back and dispose test font
SelectObject( pdx.hDC, oldFont );
DeleteObject( font );
// adjust font height
lfHeight *= s.cy / ( rcText.bottom - rcText.top );
// now we can create proper font
font = CreateFont( lfHeight, 0, 0, 0,
FW_BOLD, TRUE, FALSE, FALSE,
0, 0, 0,
0, 0, L"Microsoft Sans Serif" );
oldFont = SelectFont( pdx.hDC, font );
// draw text in test rectangle
DrawTextEx( pdx.hDC,
L"Хидрогеотермална енергија Хидрогеотермална енерги",
wcslen( L"Хидрогеотермална енергија Хидрогеотермална енерги" ),
&rcText, DT_CENTER | DT_WORDBREAK | DT_NOCLIP, NULL );
//============== draw a testing grid ===============//
// draw vertical lines of the grid
for( int i = 0; i <= pageWidth; i += pageWidth / 4 )
{
MoveToEx( pdx.hDC, i, 0, NULL );
LineTo( pdx.hDC, i, pageHeight );
}
// draw horizontal lines of the grid
for( int j = 0; j <= pageHeight; j += pageHeight / 10 )
{
MoveToEx( pdx.hDC, 0, j, NULL );
LineTo( pdx.hDC, pageWidth, j );
}
// no need for pen anymore so delete it
SelectObject( pdx.hDC, oldPen );
DeleteObject( hPen );
// no need for font, delete it
SelectFont( pdx.hDC, oldFont );
DeleteFont( font );
if( EndPage( pdx.hDC ) < 0 )
// for now pop a message box saying something went wrong
MessageBox( hWnd, L"EndDoc failed!", L"Error", MB_OK );
}
EndDoc( pdx.hDC );
}
}
if (pdx.hDevMode != NULL)
GlobalFree(pdx.hDevMode);
if (pdx.hDevNames != NULL)
GlobalFree(pdx.hDevNames);
if (pdx.lpPageRanges != NULL)
GlobalFree(pPageRanges);
if (pdx.hDC != NULL)
DeleteDC(pdx.hDC);
return hResult;
}
To use this function, just launch it on button press/menu selection or whatever.
The results in XPS seem consistent, but I get strange results in MS OneNote 2007 which following images illustrate:
Font size is 14 :
Font size is 20 :
Font size is 20, but scaling from the above function was applied :
END OF EDIT
EDITED on July, 6th 2014:
The third picture from above edit was the result of GDI using default height value because the result of my mathematical adjustment for font height was 0. Once zero is passed to CreateFont mentioned behavior is expected.
After performing proper casting from double to int I got nearly perfect output -> last letter in the string barely exceeds the limit. I will continue to try improving this formula since I believe is promising. If anybody has another mathematical solution feel free to post it.
END OF EDIT
If further info / edit is required, leave a comment and I will react as soon as possible.
There are multiple issues involved.
The biggest problem I see is in this line:
lfHeight *= s.cy / ( rcText.bottom - rcText.top );
These are all integers. In C and C++, division with integers results in truncation toward zero. So if the result of the division "should" be 3.7, you'll end up with 3, which can be a pretty crude approximation.
Another problem is that GetTextExtentPoint32 does not wrap text, but DrawText does. So you're measuring the text as though you're going to print it as a single line, and you actually draw it as multiple lines. Instead of using GetTextExtendPoint32, you can measure the height with DrawText by DT_CALCRECT flag.
Putting these together, you want to measure your text like this:
WCHAR szText[] = L"Хидрогеотермална енергија Хидрогеотермална енерги";
RECT rcText;
rcText.left = 0;
rcText.top = 0;
rcText.right = pageWidth / 4;
rcText.bottom = top;
const DWORD options = DT_CENTER | DT_WORDBREAK | DT_NOCLIP;
DrawTextEx( pdx.hDC, szText, -1, &rcText, options | DT_CALCRECT, NULL);
// Because we used DT_CALCRECT, the DrawTextEx call didn't draw anything,
// but it did adjust the bottom of rcText to account for the actual height.
double actual_height = static_cast<double>(rcText.bottom - rcText.top);
double desired_height = pageHeight / 10.0;
double ratio = desired_heigth / actual_height;
// Scale the font height by the ratio, and round it off to the nearest int.
lf.lfHeight = static_cast<int>(lf.lfHeight * ratio + 0.5);
Okay. Basically, I start off with the suggested pointSize (14 in your code) and try to draw the text using the supplied bounding rect. If the text is too large, I go into an iterative loop that decreases the pointsize and measures again until the text will fit into the bounding rect.
If, on the other hand, the text is 'too small' I go into a loop that gradually increases it's size until it is too large. Once I reach this point, I decrease the point-size by 2 and return.
The reduction by 2 is a kludge or hack. I noticed that at times the size was reported as being equal to or smaller than the reported size of the bounding rect, yet still some characters would protrude past the edge of the bounding rect.
A better solution would make use of the DrawTextEx function to both calculate the size and draw the text. This would be better since you could make use of the iLeftmargin and iRightMargin members of the DRAWTEXTPARAMS struct that is passed to that function. Whether you wished to have a margin on each side, or simply wanted to add a single character's width, that you then halved when drawing the text would depend entirely on the desired outcome. I also added the DT_EXTERNALLEADING flag to obtain a small margin above/below the text, though there isn't one for vertical padding, so you'd have to make use of the margin attributes I mention.
Since the DT_VCENTER flag doesn't work with multi-line text, you'd also need to vertically offset the text yourself if you wished it to be vertically centered. You'd just have to offset the rect used for actually drawing the text by half of the difference between the area bounding rect's height and the text bounding rect's height.
I could have used a function like this for a few projects, so thanks for the impetus to actually exercise the grey matter and work it out!
Lastly, I used an interactive demo - one that responded to the WM_PAINT message of a (empty) dialog box. Since a HDC can be treated more-or-less the same whether it be for a printer or the screen, it provided a much quicker way of investigating the result.
Output when plugged into your code: (via cutePDF virtual printer)
Code:
int rectWidth(RECT &r)
{
return (r.right - r.left) + 1;
}
int rectHeight(RECT &r)
{
return (r.bottom - r.top) + 1;
}
void measureFunc(int pointSize, HDC hdc, RECT &pRectBounding, WCHAR *textToDraw, WCHAR *fontFaceName, int &resultWidth, int &resultHeight)
{
int pixelsPerInchY = GetDeviceCaps(hdc, LOGPIXELSY);
int logHeight = -MulDiv(pointSize, pixelsPerInchY, 72);
RECT tmpRect = pRectBounding;
HFONT old, tmp = CreateFont( logHeight, 0, 0, 0, FW_BOLD, TRUE, FALSE, FALSE, 0, 0, 0, 0, 0, fontFaceName );
old = (HFONT)SelectObject(hdc, tmp);
DrawText(hdc, textToDraw, -1, &tmpRect, DT_CENTER | DT_WORDBREAK | DT_NOCLIP | DT_CALCRECT| DT_EXTERNALLEADING );
SelectObject(hdc, old);
DeleteObject(tmp);
resultWidth = rectWidth(tmpRect);
resultHeight = rectHeight(tmpRect);
}
HFONT getMaxFont(HDC hdc, WCHAR *fontName, WCHAR *textToDraw, RECT boundingRect)
{
int maxWidth = rectWidth(boundingRect), maxHeight = rectHeight(boundingRect);
int curWidth, curHeight, pointSize=14;
measureFunc(pointSize, hdc, boundingRect, textToDraw, fontName, curWidth, curHeight);
if ( (curWidth>maxWidth) || (curHeight>maxHeight) )
{
bool tooLarge = true;
while (tooLarge)
{
pointSize--;
measureFunc(pointSize, hdc, boundingRect, textToDraw, fontName, curWidth, curHeight);
if ((curWidth>maxWidth)||(curHeight>maxHeight))
tooLarge = true;
else
tooLarge = false;
}
}
else
{
bool tooSmall = true;
while (tooSmall)
{
pointSize++;
measureFunc(pointSize, hdc, boundingRect, textToDraw, fontName, curWidth, curHeight);
if ( (curWidth<maxWidth) && (curHeight<maxHeight) )
tooSmall = true;
else
tooSmall = false;
}
if ((curWidth>maxWidth) || (curHeight>maxHeight))
{
pointSize-=2;
}
}
int pixelsPerInchY = GetDeviceCaps( hdc, LOGPIXELSY );
int curFontSize;
HFONT result;
curFontSize = -MulDiv(pointSize, pixelsPerInchY, 72);
result = CreateFont(curFontSize, 0, 0, 0, FW_BOLD, TRUE, FALSE, FALSE, 0, 0, 0, 0, 0, fontName );
return result;
}
BOOL CALLBACK DlgMain(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_INITDIALOG:
{
}
return TRUE;
case WM_SIZE:
InvalidateRect(hwndDlg, NULL, true);
return 0;
case WM_ERASEBKGND:
{
RECT mRect;
GetClientRect(hwndDlg, &mRect);
HBRUSH redBrush = CreateSolidBrush(RGB(255,0,0));
FillRect((HDC)wParam, &mRect, redBrush);
DeleteObject(redBrush);
}
return true;
case WM_PAINT:
{
HDC hdc;
PAINTSTRUCT ps;
HFONT requiredFont, oldFont;
WCHAR *textToDraw = L"Хидрогеотермална енергија Хидрогеотермална енерги";
WCHAR *fontFace = L"Microsoft Sans Serif";
RECT boundingRect, dlgRect;
hdc = BeginPaint(hwndDlg, &ps);
oldFont = (HFONT)GetCurrentObject(hdc, OBJ_FONT);
GetClientRect(hwndDlg, &dlgRect);
SetRect(&boundingRect, 0,0, rectWidth(dlgRect) / 4, rectHeight(dlgRect) / 10);
FillRect(hdc, &boundingRect, (HBRUSH)GetStockObject(WHITE_BRUSH));
requiredFont = getMaxFont(hdc, fontFace, textToDraw, boundingRect);
SelectObject(hdc, requiredFont);
SetBkMode(hdc, TRANSPARENT);
DrawText(hdc, textToDraw, -1, &boundingRect, DT_CENTER | DT_WORDBREAK | DT_NOCLIP | DT_EXTERNALLEADING );
SelectObject(hdc, oldFont);
DeleteObject(requiredFont);
EndPaint(hwndDlg, &ps);
}
return false;
case WM_CLOSE:
{
EndDialog(hwndDlg, 0);
}
return TRUE;
case WM_COMMAND:
{
switch(LOWORD(wParam))
{
}
}
return TRUE;
}
return FALSE;
}

Win32 Screensaver multiple monitors with main display not leftmost

I've made a screensaver that simply scrolls user-defined text from right to left, automatically jumping back to the right if it exceeds the left boundary.
It works with multiple monitors flawlessly, barring one exception: if the 'Main Display' is on the right (i.e. Monitor #2 is primary), then I do not get the scrolling text, however the monitor IS blacked out by the code. If the main display is #1, there's no problem.
I've been poring over the code for hours and cannot identify at what stage the issue arises; I can confirm the text is in the right position (I inserted logging code that verifies its current position), but it's as if one of the API calls simply erases it. I've read the documentation for them and all looks ok.
I create a custom DC in WM_CREATE via:
if (( hDC = CreateDC(TEXT("DISPLAY"), NULL, NULL, NULL)) == NULL )
To prevent flicker, I create compatible objects to update:
void
TickerScreensaver::Paint_Prep(HDC hDC)
{
_devcon_mem = CreateCompatibleDC(hDC);
_devcon_orig = hDC;
_bmp_mem = CreateCompatibleBitmap(hDC, _width, _height);
}
and when painting in WM_PAINT (after BeginPaint, etc.), do a bit-block transfer to the actual device context:
void
TickerScreensaver::Paint(HDC hDC, RECT rect)
{
_bmp_orig = (HBITMAP)SelectObject(_devcon_mem, _bmp_mem);
FillRect(_devcon_mem, &rect, (HBRUSH)GetStockObject(BLACK_BRUSH));
if ( _gdiplus_token != NULL )
{
Graphics graphics(_devcon_mem);
SolidBrush brush(cfg.display.font_colour);
FontFamily font_family(cfg.display.font_family.c_str());
Font font(&font_family, cfg.display.font_size, FontStyleRegular, UnitPixel);
PointF point_f((f32)cfg.display.text_pos.x, (f32)cfg.display.text_pos.y);
RectF layout_rect(0, 0, 0, 0);
RectF bound_rect;
graphics.SetTextRenderingHint(TextRenderingHintAntiAlias);
graphics.MeasureString(cfg.display.text.c_str(), cfg.display.text.length(), &font, layout_rect, &bound_rect);
cfg.display.offset.x = (DWORD)(0 - bound_rect.Width);
cfg.display.offset.y = (DWORD)(bound_rect.Height / 2);
graphics.DrawString(cfg.display.text.c_str(), cfg.display.text.length(), &font, point_f, &brush);
}
BitBlt(hDC, 0, 0, _width, _height, _devcon_mem, 0, 0, SRCCOPY);
SelectObject(_devcon_mem, _bmp_orig);
}
I calculate the dimensions like so:
void
TickerScreensaver::GetFullscreenRect(HDC hDC, RECT *rect)
{
RECT s = { 0, 0, 0, 0 };
if ( EnumDisplayMonitors(hDC, NULL, EnumMonitorCallback, (LPARAM)&s) )
{
CopyRect(rect, &s);
s.left < 0 ?
_width = s.right + (0 + -s.left) :
_width = s.right;
s.top < 0 ?
_height = s.bottom + (0 + -s.top) :
_height = s.bottom;
}
}
Please note that the calculated width, height, etc., are all 100% accurate; it is purely the drawing code that doesn't appear to be working on the main display, only when it is on the right (which sets the origin to {0,0}, monitor #1 then being negative values). It is also reproduceable on a tri-display, with the main being in the center.
Well, turns out it is nice and simple - in Paint(), we should use a rect using the real width and height, not the one retrieved containing the negative values (the one actually retrieved from the API functions):
RECT r = { 0, 0, _width, _height };
_bmp_orig = (HBITMAP)SelectObject(_devcon_mem, _bmp_mem);
FillRect(_devcon_mem, &r, (HBRUSH)GetStockObject(BLACK_BRUSH));