I need to display some data in table format in a read-only multi-line edit control. Since the the edit control's font doesn't have even width for all texts, I could not use this formatting "%-20s", so I chose to use \t formatting (see code at the below). But that doesn't help me completely because it displays like in the image.
I tried using GetTextExtentPoint32() API but it could not find the exact width of \t. So, how do I align the texts correctly?
CString szMsg;
szMsg.Format(_T("%s\t%s\t%s\r\n\r\n%s\t%s\t%s\r\n%s\t%s\t%s\r\n%s\t%s\t%s"),
_T("ITEM"), _T("VALUE"), _T("STATUS"),
_T("XXXXXXXX"), _T("1.0001"), _T("PASSED"),
_T("YYYYYYYYYYYYYYYY"), _T("-0.0001"), _T("FAILED"),
_T("ZZZ"), _T("0.0101"), _T("PASSED")
);
this->GetDlgItem(IDC_EDIT1)->SetWindowText(szMsg);
Note:
1. The strings would be generated during run-time, so it can be of any length.
2. I don't want to use ListCtrl or ListView because I should allow the user to do copy/paste the result.
I think you are using the wrong tool for the job here.
Since you need to present some tabular data to the user, I'd prefer using a control specifically designed for that, like the list-view control (in report mode). You could just have a dialog-box with a list-view control inside, and use it to present your data to the user.
Since you marked this question using the MFC tag, you can consider the CListCtrl class (or several other enhanced list-view control classes available for free on CodeProject).
If you really want to format some text in tabular data in a "console-mode" style, you may want to create a dialog-box with a static text control (or read-only edit control) inside, and set its font to something fixed-width (non-proportional); but I consider the former list-view control approach higher quality.
The proper way to display tabular data in the multi-line edit control is to set the tab stops prior to setting the text. See EM_SETTABSTOPS message
This is similar to the now forgotten typewriter tabs, where hitting the TAB key moves caret to the nearest tabstop to the right. You will not be able to right-align numeric data though; for that you'd need to use ListView.
I've made something like this,
#define TAB_WIDTH 56
/*codes skipped*/
CString szItems[4] = { _T("ITEM"), _T("XXXXXXXX"), _T("YYYYYYYYYYYYYYYY"), _T("ZZZ") };
CString szValues[4] = { _T("VALUE"), _T("1.0010"), _T("-0.0009"), _T("0.1001") };
CString szStatus[4] = { _T("STATUS"), _T("Passed"), _T("Failed"), _T("Passed") };
int nTabs[3][4] = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } };
CDC *pDC = this->GetDC();
CFont *pOldFont = pDC->SelectObject(this->GetDlgItem(IDC_EDIT1)->GetFont());
SIZE sizeText;
for (int ni = 0; ni < 4; ni++)
{
GetTextExtentPoint32(pDC->GetSafeHdc(), szItems[ni], szItems[ni].GetLength(), &sizeText);
nTabs[0][ni] = sizeText.cx / TAB_WIDTH;
GetTextExtentPoint32(pDC->GetSafeHdc(), szValues[ni], szValues[ni].GetLength(), &sizeText);
nTabs[1][ni] = sizeText.cx / TAB_WIDTH;
GetTextExtentPoint32(pDC->GetSafeHdc(), szStatus[ni], szStatus[ni].GetLength(), &sizeText);
nTabs[2][ni] = sizeText.cx / TAB_WIDTH;
}
pDC->SelectObject(pOldFont);
int nBig[3] = { 0, 0, 0 };
nBig[0] = BiggestValue(nTabs[0], 4);
nBig[1] = BiggestValue(nTabs[1], 4);
nBig[2] = BiggestValue(nTabs[2], 4);
CString szDispStr = _T("");
for (int ni = 0; ni < 4; ni++)
{
szDispStr += szItems[ni];
for (int nj = nTabs[0][ni]; nj <= nBig[0]; nj++)
szDispStr += _T("\t");
szDispStr += szValues[ni];
for (int nj = nTabs[1][ni]; nj <= nBig[1]; nj++)
szDispStr += _T("\t");
szDispStr += szStatus[ni];
for (int nj = nTabs[2][ni]; nj <= nBig[2]; nj++)
szDispStr += _T("\t");
szDispStr += _T("\r\n");
if (ni == 0)
szDispStr += _T("\r\n");
}
this->GetDlgItem(IDC_EDIT1)->SetWindowTextW(szDispStr);
and the output is as expected
the edit control displayed
and the MessageBox() displayed
here TAB_WIDTH is the width of \t which I counted the pixels manually. Now I have to find the width of \t through code.
Related
I was wondering how to change the width of the area responsible for showing the current value of a Fl_Hor_Value_Slider. The thing is, I want to be able to select a year (that's 4 digits) and the space on the side of the slider is not enough to display the year number correctly.
I've looked through the docs and tried the following functions:
slider_size()
maximum()
minimum()
range()
But they don't seem do do what I want.
I feel as though I've missed something obvious.
Any ideas?
The problem is that the size is hardcoded to 35 for horizontal or 25 for vertical. You need to modify Fl_Value_Slider.cxx. In the routine draw, it has
if (horizontal()) {
bww = 35; sxx += 35; sww -= 35;
} else {
syy += 25; bhh = 25; shh -= 25;
}
This is based on a textsize_ of 10. If you change the text size, then it needs to go up based on the text size. Something like
if (horizontal()) {
int width = textsize() * 3 + 5;
bww = width; sxx += width; sww -= width;
} else {
int height = textsize() * 2 + 5;
syy += height; bhh = height; shh -= height;
}
Then rebuild the fltk library. Either keep this patch or send it to FLTK and check whenever you get an update from FLTK. To test
Fl_Hor_Value_Slider* o = new Fl_Hor_Value_Slider(10, 50, 250, 50, "");
o->tooltip("Value Slider");
o->selection_color((Fl_Color)1);
o->textsize(25);
o->value(2000)
o->range(2000, 2099);
o->precision(0);
Edit
Alternatively, create your own Hor_Value_Slider based on Fl_Hor_Value_Slider and override the draw method.
For the record: meanwhile FLTK 1.4.0 (not yet released) got new methods for sliders: value_width(int) and value_height(int), respectively. These methods let you set the width of the value field on horizontal and the height of the value field on vertical sliders.
Documentation can be found here:
https://www.fltk.org/doc-1.4/classFl__Value__Slider.html
I'm generating a custom CEdit control that allows me to set some different colors on it. It works fine until I generate on control with the style ES_PASSWORD.
In these cases, I cannot find the way to write the character I want (big black dot). Here are some codes I've tried:
First option:
int lenght = text.GetLength();
text = "";
for (int i = 0; i < lenght; i++) text.AppendChar('\u25CF');
Second option:
int lenght = text.GetLength();
text = "";
for (int i = 0; i < lenght; i++) text.Append("\u25CF");
Third option:
int lenght = text.GetLength();
text = "";
for (int i = 0; i < lenght; i++) text.AppendChar((char)"\u25CF");
I don't understand why the control doesn't display the correct character. It only displays this: <. What am I doing wrong?
UPDATE
Here is the OnPaint() method I'm using:
void CEasyEdit::OnPaint()
{
// I generate all requiered objects.
CPaintDC dc(this);
CRect ClientRect;
GetClientRect(&ClientRect);
// I define which colors I want to use.
SetDefaultColors();
// I paint the background and its borders.
CBrush brush(m_clrBack);
dc.FillRect(ClientRect, &brush);
CRect border_rect;
this->GetClientRect(border_rect);
border_rect.InflateRect(1, 1);
dc.Draw3dRect(border_rect, m_clrBack, m_clrBack);
border_rect.InflateRect(1, 1);
dc.Draw3dRect(border_rect, m_clrBack, m_clrBack);
// I redefine the size of the rect.
CRect textRect(ClientRect);
textRect.DeflateRect(4, 1);
// I define the text to draw.
CString text;
GetWindowText(text);
// If it displays a password, I change its characters.
if (GetStyle() & ES_PASSWORD)
{
// I redefine the text to show.
int lenght = text.GetLength();
wchar_t f = '1060';
text = "";
for (int i = 0; i < lenght; i++) text.Append("\u0053");
}
// I draw the text.
dc.SetTextColor(m_clrText);
dc.SetBkColor(m_clrBack);
dc.SelectObject(GetFont());
dc.DrawText(text, -1, textRect, GetStyle());
}
I was looking up CEdit::GetPasswordChar and I notice it says:
If you create the edit control with the ES_PASSWORD style, the DLL
that supports the control determines the default password character.
The manifest or the InitCommonControlsEx method determines which DLL
supports the edit control. If user32.dll supports the edit control,
the default password character is ASTERISK ('*', U+002A). If
comctl32.dll version 6 supports the edit control, the default
character is BLACK CIRCLE ('●', U+25CF). For more information about
which DLL and version supports the common controls, see Shell and
Common Controls Versions.
That said, why can't you just use CEdit::SetPasswordChar where it states:
Specifies the character to be displayed in place of the character
typed by the user. If ch is 0, the actual characters typed by the user
are displayed.
I am using a custom font called KomikaTitle. In some cases the font appears cut off on the left in the first character. This doesn't happen when I use a native font such as Arial.
The following is the code I am using:
scoreDisplayLabel = [CCLabelTTF labelWithString:#"0" dimensions:CGSizeMake(200,30) hAlignment:UITextAlignmentLeft fontName:#"KomikaTitle" fontSize:18];
scoreDisplayLabel.color = (ccc3(r,b,g));
[self addChild:scoreDisplayLabel z:2];
[scoreDisplayLabel setPosition:ccp(115,wins.height-73)];
How do I prevent this from happening? I am attaching a screenshot of the issue.
I tried messing around as suggested in http://www.cocos2d-iphone.org/forums/topic/custom-font-being-cut-off/, but no luck.
Thanks guys!
This maybe isn't a real answer, but I had the same problem with that font in an old cocos2d project I made. Just just added an extra space and a row.
This may or may not be related, but according to this source you have to include the file extension of your font. Where you have
fontName:#"KomikaTitle"
it should be
fontName:#"KomikaTitle.ttf"
for example.
If there are any android users out there using cocos2dx, this is not necessarily an easy problem to solve, but it is doable once you go down the rabbit hole. It does require editing the Cocos2dxBitmap.java file, which means that any changes made could be overrided by an update. Basically, the methods that are used to measure text are, while not incorrect, inadequate.
First, we need to add a new variable to the TextProperty
private final int mX;
Next, replace the computeTextProperty code with the following:
private static TextProperty computeTextProperty(final String pString, final int unusedWidth, final int unusedHeight, final Paint pPaint) {
final FontMetricsInt fm = pPaint.getFontMetricsInt();
final int h = (int) Math.ceil(fm.bottom - fm.top);
int maxContentWidth = 0;
final String[] lines = Cocos2dxBitmap.splitString(pString, 0,
0, pPaint);
/* Compute the max width. */
int temp = 0;
float left = 0;
for (final String line : lines) {
//get a path from text
Path path = new Path();
pPaint.getTextPath(line, 0, line.length(), 0, 0, path);
RectF bounds = new RectF();
path.computeBounds(bounds, true);
temp = (int) FloatMath.ceil(bounds.width());
//if the text extends to the left of 0
if (bounds.left < left) {
left = bounds.left;
}
if (temp > maxContentWidth) {
maxContentWidth = temp;
//extend the width to account for text rendered to the left of 0
if (left < bounds.left) {
maxContentWidth += (int) FloatMath.ceil(Math.abs(left));
}
}
}
left = Math.abs(left);
return new TextProperty(maxContentWidth, h, lines, (int) FloatMath.ceil(left));
}
What has basically happened is that we have used information returned by the text path to get if the left bound is less than 0, which would mean it would be rendered outside the bitmap. We also extend the width when there are multiple lines of text, as we are going to shift everything to match the left bounds, we need the right bounds shifted too.
Finally, replace computeX with
private static int computeX(final String pText, final int pMaxWidth,
final int pHorizontalAlignment, final int pX) {
int ret = 0;
int expectedWidth = pX + pMaxWidth;
switch (pHorizontalAlignment) {
case HORIZONTALALIGN_CENTER:
ret = expectedWidth / 2;
break;
case HORIZONTALALIGN_RIGHT:
ret = expectedWidth;
break;
case HORIZONTALALIGN_LEFT:
ret = pX;
default:
break;
}
return ret;
}
You'll have to do all the hookups yourself, but this will provide the most accurate text rendering.
I have a CRichEditCtrl (actually I have a class that is a subclass of a CRichEditCtrl, a class that I defined) that is populated by many lines of text with both horizontal and vertical scroll bars. The purpose of this control is to display a string that is searched for in a larger text along with n characters to the right and left (e.g. if the user searches for "the" then they would get a list of all the instances of "the" in the text with (if n = 100) 100 characters to the left and right of each found instance to provide context).
The query string needs to be lined up between each row. Before this program had Unicode support, just setting the font to Courier did the trick, but now that I've enabled Unicode support, this no longer works.
I've tried using monospaced fonts, but as far as I can tell, there aren't any that are for all characters. It seems to me that the latin characters all have one size, and the Chinese characters have another (I've noticed lines of text with all latin characters line up and ones with all Chinese characters line up, but ones with both do not line up).
I've also tried center aligning the text. Since the query string in each line is in the exact center, they should all line up, but I cannot seem to get this to work, the SetParaFormat call seems to just get ignored. Here's the code I used for that:
long spos, epos;
GetSel(spos, epos);
PARAFORMAT Pfm;
GetParaFormat(Pfm);
Pfm.dwMask = (Pfm.dwMask | PFM_ALIGNMENT);
Pfm.wAlignment = PFA_CENTER;
SetSel(0, -1);
SetParaFormat(Pfm);
SetSel(spos, epos);
I do this everytime text is inserted in the ctrl, but it has no affect on the program.
Is there anyway to get the query word in each line of text to line up even when there are interspersed Chinese and latin characters? (and possibly any other character set)
See http://msdn.microsoft.com/en-us/library/bb787940(v=vs.85).aspx, in particular the cTabCount and rgxTabs members of the PARAFORMAT (or PARAFORMAT2) structure, which allow you to set tabstops.
Okay so I managed to solve it. For future reference, here's what I did:
First, I tried to find a monospaced font, but I was unable to find any that were truly monospaced (had latin and chinese characters as the same width).
Next, I tried to center the text in the window. I was unable to do this until I realized that having Auto HScroll set to true (ES_AUTOHSCROLL defined for the rich edit control) caused setParaFormat to ignore me trying to center the text. After I disabled it and manually set the size of the drawable text area, I was able to center the text. Just in case anyone is curious, here's the code I used to set the width of the drawable area in the rich edit box:
CDC* pDC = GetDC();
long lw = 99999999;
SetTargetDevice(*GetDC(), lw);
I just set lw arbitrarily large so I could test to see if centering the text worked. It did not. As it turns out, when the rich edit control centers the text, it bases it off the draw width of the text, not off the number of characters. I assumed that since there were the same number of characters on either side of the query string then that would cause the string to be centered, but this was not the case.
The final solution I tried was the one suggested by ymett. After some tweaking I came up with a function called alignText() that's called after all the text has been inserted into the rich edit control. Here's the function (Note: each line in the control had tabs inserted before this function was called: one at the beginning of the line and one after the query string e.g. "string1-query-string2" becomes "\tstring1-query\t-string2")
void CFormViewConcordanceRichEditCtrl::alignText()
{
long maxSize = 0;
CFont font;
LOGFONT lf = {0};
CHARFORMAT cf = {0};
this->GetDefaultCharFormat(cf);
//convert a CHARFORMAT struct into a LOGFONT struct
if (cf.dwEffects & CFE_BOLD)
lf.lfWeight = FW_BOLD;
else
lf.lfWeight = FW_NORMAL;
if (cf.dwEffects & CFE_ITALIC)
lf.lfItalic = true;
if (cf.dwEffects & CFE_UNDERLINE)
lf.lfUnderline = true;
if (cf.dwEffects & CFE_STRIKEOUT)
lf.lfStrikeOut = true;
lf.lfHeight = cf.yHeight;
_stprintf(lf.lfFaceName, _T("%s"), cf.szFaceName);
lf.lfCharSet = DEFAULT_CHARSET;
lf.lfOutPrecision = OUT_DEFAULT_PRECIS;
font.CreateFontIndirect(&lf);
//create a display context
CClientDC dc(this);
dc.SetMapMode(MM_TWIPS);
CFont *pOldFont = dc.SelectObject(&font);
//find the line that as the longest preceding string when drawn
for(int i = 0; i < m_pParent->m_nDataArray; i++)
{
CString text = m_pParent->DataArray[i].text.Left(BUFFER_LENGTH + m_pParent->generateText.GetLength());
text.Replace(_T("\r"), _T(" "));
text.Replace(_T("\n"), _T(" "));
text.Replace(_T("\t"), _T(" "));
CRect rc(0,0,0,0);
dc.DrawText(text, &rc, DT_CALCRECT);
int width = 1.0*cf.yHeight/fabs((double)rc.bottom - rc.top)*(rc.right - rc.left);
width = dc.GetTextExtent(text).cx;
if(width > maxSize)
maxSize = width;
}
dc.SelectObject(pOldFont);
//this calulates where to place the first tab. The 0.8 is a rought constant calculated by guess & check, it may be innacurate.
long tab = maxSize*0.8;
PARAFORMAT pf;
pf.cbSize = sizeof(PARAFORMAT);
pf.dwMask = PFM_TABSTOPS;
pf.cTabCount = 2;
pf.rgxTabs[0] = tab + (2 << 24); //make the first tab right-aligned
pf.rgxTabs[1] = tab + 25;
//this is to preserve the user's selection and scroll positions when the selection is changed
int vScroll = GetScrollPos(SB_VERT);
int hScroll = GetScrollPos(SB_HORZ);
long spos, epos;
GetSel(spos, epos);
//select all the text
SetSel(0, -1);
//this call is very important, but I'm not sure why
::SendMessage(GetSafeHwnd(), EM_SETTYPOGRAPHYOPTIONS, TO_ADVANCEDTYPOGRAPHY, TO_ADVANCEDTYPOGRAPHY);
this->SetParaFormat(pf);
//now reset the user's selection and scroll positions
SetSel(spos, epos);
::SendMessage(GetSafeHwnd(),
WM_HSCROLL,
(WPARAM) ((hScroll) << 16) + SB_THUMBPOSITION,
(LPARAM) NULL);
::SendMessage(GetSafeHwnd(),
WM_VSCROLL,
(WPARAM) ((vScroll) << 16) + SB_THUMBPOSITION,
(LPARAM) NULL);
}
Essentially what this function does is make the first tab stop right-aligned and set it at some point x to the right in the control. It then makes the second tab stop a small distance to the right of that, and makes it left-aligned. So all the text from the beginning of each line to the end of the query string (from the first \t to the second \t) is pushed toward the right against the first tab stop, and all the remaining text is pushed toward the left against the second tab stop, causing the query string to be aligned between all the lines in the control. The first part of the function finds out x (by finding out how long each line will be drawn and taking the max) and the second part sets the tab stops.
Thanks again to ymett for the solution.
I have a check box that I want to accurately measure so I can position controls on a dialog correctly. I can easily measure the size of the text on the control - but I don't know the "official" way of calculating the size of the check box and the gap before (or after) the text.
I'm pretty sure the width of the checkbox is equal to
int x = GetSystemMetrics( SM_CXMENUCHECK );
int y = GetSystemMetrics( SM_CYMENUCHECK );
You can then work out the area inside by subtracting the following ...
int xInner = GetSystemMetrics( SM_CXEDGE );
int yInner = GetSystemMetrics( SM_CYEDGE );
I use that in my code and haven't had a problem thus far ...
Short answer:
Long Version
From MSDN Layout Specifications: Win32, we have the specifications of the dimensions of a checkbox.
It is 12 dialog units from the left edge of the control to the start of the text:
And a checkbox control is 10 dialog units tall:
Surfaces and Controls Height (DLUs) Width (DLUs)
===================== ============= ===========
Check box 10 As wide as possible (usually to the margins) to accommodate localization requirements.
First we calculate the size of a horizontal and a vertical dialog unit:
const dluCheckBoxInternalSpacing = 12; //12 horizontal dlus
const dluCheckboxHeight = 10; //10 vertical dlus
Size dialogUnits = GetAveCharSize(dc);
Integer checkboxSpacing = MulDiv(dluCheckboxSpacing, dialogUnits.Width, 4);
Integer checkboxHeight = MulDiv(dluCheckboxHeight, dialogUnits.Height, 8);
Using the handy helper function:
Size GetAveCharSize(HDC dc)
{
/*
How To Calculate Dialog Base Units with Non-System-Based Font
http://support.microsoft.com/kb/125681
*/
TEXTMETRIC tm;
GetTextMetrics(dc, ref tm);
String buffer = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
Size result;
GetTextExtentPoint32(dc, buffer, 52, out result);
result.Width = (result.X/26 + 1) / 2; //div uses trunc rounding; we want arithmetic rounding
result.Height = tm.tmHeight;
return result;
}
Now that we know how many pixels (checkboxSpacing) to add, we calculate the label size as normal:
textRect = Rect(0,0,0,0);
DrawText(dc, Caption, -1, textRect, DT_CALCRECT or DT_LEFT or DT_SINGLELINE);
chkVerification.Width = checkboxSpacing+textRect.Right;
chkVerification.Height = checkboxHeight;
Bonus Reading
What's a dialog unit?
A dialog is a unit of measure based on the user's preferred font size. A dialog unit is defined such that the average character is 4 dialog units wide by 8 dialog units high:
This means that dialog units:
change with selected font
changed with selected DPI setting
are not square
Note: Any code released into public domain. No attribution required.
Sorry for resurrecting this old thread. I recently found myself wondering about the exact same question. Currently, none of the answers above give a result consistent with Windows 10 for different fonts and font sizes, especially in high-DPI environments.
Instead, it seems that the correct result is obtained by
SIZE szCheckBox;
GetThemePartSize(hTheme, hDC, BP_CHECKBOX, CBS_UNCHECKEDNORMAL, &rcBackgroundContent, TS_TRUE, &szCheckBox);
for the size of the checkbox itself. And
SIZE szZeroCharacter;
GetTextExtentPoint32(hDC, L"0", 1, &szZeroCharacter);
int iGapWidth = szZeroCharacter.cx / 2;
for the width of the gap. After trying a lot of different methods inspired by the posts above, I found L"0" in the dissembly of comctl32.dll. And while it looks like a joke to me (not necessarily a good one), I suspect it's a holdover from the old days when this might have been a good enough approximation of 2DLU.
Disclaimer: While I tested the result with various fonts and different sizes on Windows 10, I have not attempted to verify that it also holds on any other (older) version of the operating system.
It is a shame that Microsoft did not provide a way to know this for sure. I was struggling with the same question and the answer provided above is not complete. The main problem with it is that if the font of the dialog window is set to something other than the default size, that solution will not work because checkboxes will be resized.
Here's how I solved this issue (it is just an approximation that seems to have worked for me). The code is for MFC project.
1 - Create two test controls on your form, a checkbox and a radio box:
2 - Define the following custom struct:
struct CHECKBOX_DIMS{
int nWidthPx;
int nHeightPx;
int nSpacePx; //Space between checkbox and text
CHECKBOX_DIMS()
{
nWidthPx = 0;
nHeightPx = 0;
nSpacePx = 0;
}
};
3 - Call the following code when form initializes for each of the test controls (that will measure them and remove them so that end-users don't seem them):
BOOL OnInitDialog()
{
CDialog::OnInitDialog();
//Calculate the size of a checkbox & radio box
VERIFY(GetInitialCheckBoxSize(IDC_CHECK_TEST, &dimsCheckBox, TRUE));
VERIFY(GetInitialCheckBoxSize(IDC_RADIO_TEST, &dimsRadioBox, TRUE));
//Continue with form initialization ...
}
BOOL GetInitialCheckBoxSize(UINT nCtrlID, CHECKBOX_DIMS* pOutCD, BOOL bRemoveCtrl)
{
//Must be called initially to calculate the size of a checkbox/radiobox
//'nCtrlID' = control ID to measure
//'pOutCD' = if not NULL, receives the dimensitions
//'bRemoveCtrl' = TRUE to delete control
//RETURN:
// = TRUE if success
BOOL bRes = FALSE;
//Get size of a check (not exactly what we need)
int nCheckW = GetSystemMetrics(SM_CXMENUCHECK);
int nCheckH = GetSystemMetrics(SM_CYMENUCHECK);
//3D border spacer (not exactly what we need either)
int nSpacerW = GetSystemMetrics(SM_CXEDGE);
//Get test checkbox
CButton* pChkWnd = (CButton*)GetDlgItem(nCtrlID);
ASSERT(pChkWnd);
if(pChkWnd)
{
CRect rcCheckBx;
pChkWnd->GetWindowRect(&rcCheckBx);
//We need only the height
//INFO: The reason why we can't use the width is because there's
// an arbitrary text followed by a spacer...
int h = rcCheckBx.Height();
CDC* pDc = pChkWnd->GetDC();
if(pDc)
{
//Get horizontal DPI setting
int dpiX = pDc->GetDeviceCaps(LOGPIXELSX);
//Calculate
if(pOutCD)
{
//Use height as-is
pOutCD->nHeightPx = h;
//Use height for the width
pOutCD->nWidthPx = (int)(h * ((double)nCheckW / nCheckH));
//Spacer is the hardest
//INFO: Assume twice and a half the size of 3D border &
// take into account DPI setting for the window
// (It will give some extra space, but it's better than less space.)
// (This number is purely experimental.)
// (96 is Windows DPI setting for 100% resolution setting.)
pOutCD->nSpacePx = (int)(nSpacerW * 2.5 * dpiX / 96.0);
}
//Release DC
pChkWnd->ReleaseDC(pDc);
if(bRemoveCtrl)
{
//Delete window
bRes = pChkWnd->DestroyWindow();
}
else
{
//Keep the window
bRes = TRUE;
}
}
}
return bRes;
}
4 - Now you can easily resize any checkbox or radio box by calling this:
//Set checkbox size & new text
VERIFY(SetCheckBoxTextAndSize(this, IDC_CHECK_ID, &dimsCheckBox, L"New text") > 0);
//Just resize radio box
VERIFY(SetCheckBoxTextAndSize(this, IDC_RADIO_ID, &dimsRadioBox, NULL) > 0);
int SetCheckBoxTextAndSize(CWnd* pParWnd, UINT nCheckBoxID, CHECKBOX_DIMS* pDims, LPCTSTR pNewText)
{
//Set size of the checkbox/radio to 'pNewText' and update its size according to its text
//'pParWnd' = parent dialog window
//'nCheckBoxID' = control ID to resize (checkbox or radio box)
//'pDims' = pointer to the struct with checkbox/radiobox dimensions
//'pNewText' = text to set, or NULL not to change the text
//RETURN:
// = New width of the control in pixels, or
// = 0 if error
int nRes = 0;
ASSERT(pParWnd);
ASSERT(pDims);
CButton* pChkWnd = (CButton*)pParWnd->GetDlgItem(nCheckBoxID);
ASSERT(pChkWnd);
if(pChkWnd)
{
CDC* pDc = pChkWnd->GetDC();
CFont* pFont = pChkWnd->GetFont();
if(pDc)
{
if(pFont)
{
//Make logfont
LOGFONT lf = {0};
if(pFont->GetLogFont(&lf))
{
//Make new font
CFont font;
if(font.CreateFontIndirect(&lf))
{
//Get font from control
CFont* pOldFont = pDc->SelectObject(&font);
//Get text to set
CString strCheck;
if(pNewText)
{
//Use new text
strCheck = pNewText;
}
else
{
//Keep old text
pChkWnd->GetWindowText(strCheck);
}
//Calculate size
RECT rc = {0, 0, 0, 0};
::DrawText(pDc->GetSafeHdc(), strCheck, strCheck.GetLength(), &rc, DT_CALCRECT | DT_NOPREFIX | DT_SINGLELINE);
//Get text width
int nTextWidth = abs(rc.right - rc.left);
//See if it's valid
if(nTextWidth > 0 ||
(nTextWidth == 0 && strCheck.GetLength() == 0))
{
//Get location of checkbox
CRect rcChk;
pChkWnd->GetWindowRect(&rcChk);
pParWnd->ScreenToClient(rcChk);
//Update its size
rcChk.right = rcChk.left + pDims->nWidthPx + pDims->nSpacePx + nTextWidth;
//Use this line if you want to change the height as well
//rcChk.bottom = rcChk.top + pDims->nHeightPx;
//Move the control
pChkWnd->MoveWindow(rcChk);
//Setting new text?
if(pNewText)
{
pChkWnd->SetWindowText(pNewText);
}
//Done
nRes = abs(rcChk.right - rcChk.left);
}
//Set font back
pDc->SelectObject(pOldFont);
}
}
}
//Release DC
pChkWnd->ReleaseDC(pDc);
}
}
return nRes;
}
I'd like to give my 2 cents on the matter since iv spent an entire day working on an accurate solution for this problem that takes DPI awareness and fonts into consideration.
First define the checkbox's size in units.
#define CHECKBOX_INTERNAL_SIZE 12
Then i defined a function for converting units to pixels. NOTE: MulDiv may work just as good.
double dpi_MulDiv(double nNumber, double nNumerator, double nDenominator)
{
return (nNumber * nNumerator) / nDenominator;
}
Finally the function that does the magic. SEE the code comments for details.
//
// Get the minimum size of the Checkbox.
// NOTE: The font of the control must be set before calling this function.
//
SIZE dpi_GetCheckBoxWidth(HWND hWnd, int monitorDpi)
{
HDC dc;
HFONT hFont;
HFONT oldFont;
TEXTMETRIC tm;
double checkboxSize;
double whiteSpace;
WCHAR sourceString[128];
RECT txtRect;
SIZE size;
dc = GetDC(hWnd);
// Note that GetDC returns an uninitialized DC, which has "System" (a bitmap font) as the default font; thus the need to select a font into the DC.
hFont = (HFONT)SendMessage(hWnd, WM_GETFONT, 0, 0);
oldFont = (HFONT)SelectObject(dc, hFont);
// Get the Checkbox width.
checkboxSize = round(dpi_MulDiv(CHECKBOX_INTERNAL_SIZE, monitorDpi, 96));
// Get the space between the Checkbox and text.
GetTextMetrics(dc, &tm);
whiteSpace = round((double)tm.tmAveCharWidth / 2.0f);
// Get the Text width.
txtRect = { 0, 0, 0, 0 };
if (GetWindowTextW(hWnd, sourceString, 128) != 0)
{
DrawTextW(dc, sourceString, -1, &txtRect, DT_CALCRECT | DT_LEFT | DT_SINGLELINE);
}
// Cleanup.
SelectObject(dc, oldFont);
ReleaseDC(hWnd, dc);
// Done.
size.cx = (LONG)checkboxSize + (LONG)whiteSpace + txtRect.right + 3;
size.cy = ((LONG)checkboxSize < txtRect.bottom) ? txtRect.bottom : (LONG)checkboxSize;
return size;
}
I added + 3 on the last line that computes the width as a way to adjust for little irregularities. Feed back on this is welcomed. Iv only tested on Windows 10 thus far with different fonts and sizes.
This code doesn't work on Win7 with scaled UI (fonts 125% larger or 150% larger). The only thing that seems to work is:
int WID = 13 * dc.GetDeviceCaps(LOGPIXELSX) / 96;
int HEI = 13 * dc.GetDeviceCaps(LOGPIXELSY) / 96;
Ok dudes my way is maybe not the fastes to use in runtime, but it works for me in any case i have tested so far.
In the beginnin of my proggys i put in a function to get the size and store it in a global variable (yeah i have heard this would be bad, but i dont care about this)
here the explanation:
Create a treeview (invisible if u want)
Create an imagelist with atleast 1 image inside (size 16x16)
Set the imagelist to the treeview ("TVSIL_NORMAL")
Get the "TVSIL_STATE" imagelist from the treeview (u have to create "TVSIL_NORMAL" before, otherwise this one will fail!)
Use ImageList_GetIconSize(..) and store the size. Wow, the checkboxs and the radio-buttons have the same size as the state icons of the treeview. Now u have what u want!
Destroy the "TVSIL_NORMAL" imagelist
Destroy the treeview
this code needs only a few microseconds at the beginning of my proggies and i can use the value everytime i need it.
Preamble:
I had the same question while trying to determine the needed size of the checkbox control for a given text and found that the existing answers didn't really work for me, for several reasons:
SM_CXMENUCHECK doesn't account for the gap. In fact, I'm not convinced this is even for regular checkboxes, although it may have the same value. It may also be dependent on visual styles being enabled.
The other answers were overly complicated and felt a bit hacky (no disrespect intended, it is MS that don't make this easy).
The stated 12DLU layout was very helpful, although again feels arbitrary without a system metric to rely on.
The answers I tried still didn't yield a high enough pixel value to stop the checkbox text from wrapping.
My investigation:
I looked at how Wine reproduces the behavior and found that it also gives the same results as simply assuming 12DLU. However, the text still wrapped unless I added an extra 3 pixels to the width (even though the text should fit fine without). I also noticed that GetTextExtentPoint32 yields a value of 3 for an empty string (hmmm...)
Turning off the BS_MULTILINE style obviously stopped the text wrapping. My guess is that DrawTextW's word wrapping calculations are imperfect.
At this point I decided that the simplest solution was to just add 1 extra space to GetTextExtentPoint32, so that there would definitely be enough pixels. The over-estimate of a couple of pixels was acceptable to me.
Note that this all assumes your application is manifested as DPI aware. Otherwise I found the checkbox appeared much larger on some Windows 7 systems (not all though).
My (mostly Wine's) solution:
// This code gets the size of a piece of text and adds the size of a
// checkbox and gap. Note that this is very rough code with no error handling.
BOOL isCheckbox = TRUE;
HWND dialog = ... // Your control or dialog
HFONT font = ... // The font your control will use if it hasn't been set yet
PTCHAR text = ... // Your text
HFONT currentFont;
SIZE size;
HDC dc = GetDC(dialog);
if (!font) {
font = (HFONT)SendMessage(dialog, WM_GETFONT, 0, 0);
}
currentFont = (HFONT)SelectObject(dc, font); // NB: You should add error handling here
if (isCheckbox) {
// Or you can disable BS_MULTILINE
_tcscat(text, TEXT(" ")); // NB: This assumes text is allocated for +1 char
}
GetTextExtentPoint32(dc, text, _tcslen(text), &size); // NB: You should add error handling here
if (isCheckbox) {
int checkBoxWidth = 12 * GetDeviceCaps(dc, LOGPIXELSX ) / 96 + 1;
int checkBoxHeight = 12 * GetDeviceCaps(dc, LOGPIXELSY ) / 96 + 1;
int textOffset;
GetCharWidthW(dc, '0', '0', &textOffset);
textOffset /= 2;
size->cx += checkBoxWidth + textOffset;
if (size->cy < checkBoxHeight) {
size->cy = checkBoxHeight;
}
}
if (currentFont) {
SelectObject(dc, currentFont);
}
ReleaseDC(dialog, dc);