如何获取复选框中勾选和间隙的大小?

17

我有一个复选框需要准确测量,以便在对话框上正确定位控件。我可以轻松测量控件上文本的大小,但是我不知道计算复选框尺寸和文本之前(或之后)间隙的“官方”方法。

8个回答

19

我很确定复选框的宽度等于

int x = GetSystemMetrics( SM_CXMENUCHECK );
int y = GetSystemMetrics( SM_CYMENUCHECK );

您可以通过减去以下内容来计算内部面积...

   int xInner = GetSystemMetrics( SM_CXEDGE );
   int yInner = GetSystemMetrics( SM_CYEDGE );

我在我的代码中使用了这个,到目前为止还没有遇到任何问题...


1
在我这里似乎一切正常:) (有趣的是它是“菜单检查”,而没有普通复选框的选项?) - Mark Ingram
1
我知道这是一个旧的帖子,我是通过谷歌找到它的。不幸的是,这个答案并不完全正确。请看下面我的解释... - c00000fd

10
简短回答:

enter image description here

长版本

从MSDN 布局规范:Win32,我们可以得到复选框尺寸的规格。

从控件的左边缘到文本开始的位置有12个对话框单位

enter image description here

复选框控件的高度为10个对话框单位:

表面和控件 高度(DLU) 宽度(DLU)
复选框 10 尽可能宽(通常到边缘),以适应本地化要求。

首先,我们计算水平和垂直对话框单位的大小:

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);

使用方便的助手函数:
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;
}

现在我们知道要添加多少像素(checkboxSpacing),我们按照正常方式计算标签的大小:
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;

enter image description here

额外阅读

什么是对话单位?

对话单位是基于用户首选字体大小的度量单位。对话单位的定义是,平均字符的宽度为4个对话单位,高度为8个对话单位:

enter image description here

这意味着对话框单位: - 随所选字体而变化 - 随所选 DPI 设置而变化 - 不是正方形
注意:任何发布到公共领域的代码都不需要归属。

所以,如果复选框的高度始终为10个单位,间距始终为12个单位,那么为什么不直接使用从GetTextExtentPoint32获取的文本高度,然后从中计算宽度呢? checkboxSpacing = (12 * Height) / 10; - DaedalusAlpha
1
@DaedalusAlpha 因为“1个水平dlu”和“1个垂直dlu”在像素上是不同大小的。 - Ian Boyd

3

抱歉,我重新启动了这个旧的帖子。最近,我发现自己对相同的问题产生了疑问。目前,以上回答中没有一个能够在不同字体和字体大小,特别是在高DPI环境下与Windows 10一致。

相反,正确的结果似乎可以通过以下方式获得:

SIZE szCheckBox;
GetThemePartSize(hTheme, hDC, BP_CHECKBOX, CBS_UNCHECKEDNORMAL, &rcBackgroundContent, TS_TRUE, &szCheckBox);

复选框自身的大小。以及。
SIZE szZeroCharacter;
GetTextExtentPoint32(hDC, L"0", 1, &szZeroCharacter);
int iGapWidth = szZeroCharacter.cx / 2;

针对间隙的宽度。在尝试了上面帖子中提到的各种不同方法后,我在comctl32.dll的反汇编代码中找到了L"0"。虽然这看起来像是一个笑话(不一定是好笑的),但我怀疑这是从旧时代遗留下来的,当时这可能已经足够近似于2DLU。

免责声明:虽然我在Windows 10上使用各种字体和不同大小的文本测试了结果,但我并没有尝试验证它是否也适用于任何其他(旧版)操作系统。


关于hTheme的创建和rcBackgroundContent怎么办?这是无法编译的代码,因此无法使用! - The Bitman

2
很遗憾,微软没有提供一种确切的方法来知道这个问题。我也曾经面临同样的问题,上面提供的答案并不完整。主要问题在于,如果对话框窗口的字体设置为除默认大小以外的其他大小,那么该解决方案将不起作用,因为复选框将被调整大小。
以下是我解决这个问题的方法(只是一个近似值,看起来对我有用)。代码适用于MFC项目。
1- 在您的表单上创建两个测试控件,一个复选框和一个单选框:

enter image description here

2 - 定义以下自定义结构体:

struct CHECKBOX_DIMS{
    int nWidthPx;
    int nHeightPx;
    int nSpacePx;       //Space between checkbox and text

    CHECKBOX_DIMS()
    {
        nWidthPx = 0;
        nHeightPx = 0;
        nSpacePx = 0;
    }
};

3 - 当表单初始化时,针对每个测试控件调用以下代码(这将测量它们并将其移除,以便最终用户不会看到它们):

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- 现在你可以通过调用以下代码轻松地重新调整任何复选框或单选框的大小:

//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;
}

1
我希望在这个问题上提供我的意见,因为我花了一整天的时间来解决这个问题,考虑了DPI感知和字体等因素。
首先,在单位中定义复选框的大小。
#define CHECKBOX_INTERNAL_SIZE 12
然后,我定义了一个将单位转换为像素的函数。注意:MulDiv也可能同样有效。
double dpi_MulDiv(double nNumber, double nNumerator, double nDenominator)
{
    return (nNumber * nNumerator) / nDenominator;
}

最后是实现魔法的函数。请查看代码注释以获取详细信息。

//
// 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;
}

我在计算宽度的最后一行添加了+ 3,作为调整小不规则性的一种方式。欢迎提供反馈。目前仅在Windows 10上使用不同字体和大小进行了测试。


0

这段代码在 UI 缩放为 125% 大小或 150% 大小的 Win7 上无法正常工作。唯一有效的方式似乎是:

int WID = 13 * dc.GetDeviceCaps(LOGPIXELSX) / 96; 
int HEI = 13 * dc.GetDeviceCaps(LOGPIXELSY) / 96;

0

好的小伙伴们,我的方法可能不是运行时最快的,但在我测试过的任何情况下都适用。

在我的程序开头,我加入了一个函数来获取大小并将其存储在全局变量中(是的,我听说这样做会很糟糕,但我不在乎)

以下是解释:

  1. 创建一个树形视图(如果需要,则将其设置为不可见)
  2. 创建至少包含1个图像(大小为16x16)的映像列表
  3. 将映像列表设置为树形视图("TVSIL_NORMAL")
  4. 从树形视图中获取“TVSIL_STATE”图像列表(必须先创建“TVSIL_NORMAL”,否则此项将失败!)
  5. 使用ImageList_GetIconSize(..)并存储大小。哇,复选框和单选按钮的大小与树形视图的状态图标相同。现在你有了想要的东西!
  6. 销毁“TVSIL_NORMAL”图像列表
  7. 销毁树形视图

这段代码只需要几微秒,我就可以在需要时随时使用该值。


0

前言:
在尝试确定给定文本所需的复选框控件大小时,我遇到了同样的问题,并发现现有的答案对我来说并不真正有效,原因如下:

  • SM_CXMENUCHECK 没有考虑间隙。实际上,我不确定这甚至是针对常规复选框的,尽管它可能具有相同的值。它也可能依赖于启用视觉样式。
  • 其他答案过于复杂,感觉有点繁琐(无意冒犯,这是微软没有让这变得容易)。
  • 所述的12DLU布局非常有帮助,但再次感觉是没有系统度量标准的任意选择。
  • 我尝试的答案仍然没有产生足够高的像素值,以防止复选框文本换行。

我的调查结果:
我研究了Wine如何复制这种行为,并发现它也会产生与简单假设12DLU相同的结果。然而,文本仍然换行,除非我将宽度增加3个像素(即使文本应该适合而不需要)。我还注意到GetTextExtentPoint32对于空字符串返回值为3(嗯...)
关闭BS_MULTILINE样式显然可以停止文本换行。我猜测DrawTextW的单词换行计算是不完美的。
在这一点上,我决定最简单的解决方案就是在GetTextExtentPoint32中添加1个额外的空格,以确保有足够的像素。几个像素的过度估计对我来说是可以接受的。

请注意,这一切都假定您的应用程序被表现为DPI感知。否则,我发现复选框在某些Windows 7系统上显示得更大(但并非所有系统都是如此)。

我的(主要是Wine的)解决方案:

// 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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接