【问题标题】:Calculate ideal font size, based on the paper size and maximum allowed text length根据纸张大小和允许的最大文本长度计算理想的字体大小
【发布时间】:2014-08-19 12:54:36
【问题描述】:

我有在纸上绘制网格的打印代码。

网格有 4 列,它们的水平长度相等。单元格的高度是纸张尺寸的十分之一。总行数未知,但我知道至少会有一行。

每个单元格具有相同的物理尺寸-> 宽度是纸张宽度的四分之一,高度是纸张高度的十分之一。 单元格最多可容纳 50 个字符。

我面临的问题是选择正确的字体大小,以便最大长度的文本可以适合单元格。

浏览 MSDN 文档和WinAPI 示例,我看到他们使用GetTextExtPoint32 用于类似目的,但这仅在字体已经存在并且被选择到设备上下文时才有效,这里不是这样。

我唯一想到的是创建“虚拟字体”,查看示例文本是否可以放入单元格,然后在测试失败时调整其大小。我还发现 this blog 推荐了解决此问题的有趣方法,但由于缺乏经验,我无法确定“这是否是正确的方法”。

您能为我的问题推荐一个正确的解决方案吗?

2014 年 6 月 30 日编辑:

以下是绘制网格并将左上单元格绘制为浅灰色的示例函数,因为该单元格将包含示例文本。这样我们就可以直观地验证我们的绘图代码是否成功:

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

要使用此功能,只需按下按钮/菜单选择或其他方式启动它。

XPS 中的结果似乎一致,但我在 MS OneNote 2007 中得到了奇怪的结果,如下图所示:

字体大小为 14:

字体大小为 20:

字体大小为 20,但应用了上述功能的缩放:

编辑结束

2014 年 7 月 6 日编辑:

上面编辑的第三张图片是GDI使用默认高度值的结果,因为我对字体高度进行数学调整的结果是0。一旦zero is passed to CreateFont 提到的行为是预期的。

doubleint 执行正确的转换后,我得到了几乎完美的输出 -> 字符串中的最后一个字母几乎没有 超过了限制。我将继续尝试改进这个公式,因为我相信很有希望。如果有人有其他数学解决方案,请随时发布。

编辑结束

如果需要进一步的信息/编辑,请发表评论,我会尽快做出反应。

【问题讨论】:

  • 创建一个虚拟字体并获取最大长度字符串的长度。如果“不合适”,请不要丢弃它——用它来计算更接近的值!如果文本是 n 像素宽并且您的单元格是 m 像素,则将字体大小缩小到 m/n
  • @Jongware:文本确实适合单元格,但结果不是我所期望的。我用DrawTextExt画出测试串,测试串有几个字。文本必须垂直居中。虽然整个测试字符串都适合单元格,但垂直居中是不正确的。如果需要,我将使用小型演示 sn-p 更新我的帖子。 也许我误解了你,或者错误地应用了你的建议。最好的问候。
  • 听起来好像我误解了您对“选择适当的字体大小以便最大长度的文本可以放入单元格”的解释。我在您的帖子中看不到“垂直居中”的提及,所以也许您应该编辑它。(我也在想,也许您拥有的草图或屏幕截图会有所帮助。)
  • @Jongware:我相信你是对的,但现在已经很晚了。另外,我想真正付出努力,以便我的编辑可以具有建设性,所以我明天会进行更新(包括截图)。最好的问候
  • @Jongware:我已按照您的指示编辑了我的帖子。至于垂直居中,我相信一个简单的数学会帮助我。现在我的主要问题显示在我编辑的最后一张图片中。最好的问候。

标签: c++ winapi fonts gdi+ gdi


【解决方案1】:

涉及多个问题。

我看到的最大问题在这一行:

lfHeight *= s.cy / ( rcText.bottom - rcText.top );

这些都是整数。在 C 和 C++ 中,整数除法会导致向零截断。因此,如果除法的结果“应该”是 3.7,那么您最终会得到 3,这可能是一个非常粗略的近似值。

另一个问题是 GetTextExtentPoint32 不换行,但 DrawText 可以。因此,您正在测量文本,就好像您要将其打印为单行一样,而实际上您将其绘制为多行。除了使用 GetTextExtendPoint32,您可以通过 DT_CALCRECT 标志使用 DrawText 测量高度。

把这些放在一起,你想像这样测量你的文本:

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

【讨论】:

  • 我试过你的代码,结果不是我需要的。不幸的是,DrawTextExt(和DrawText)在文本长度大于矩形宽度时扩展矩形宽度(参见DT_CALCRECT 的文档):If the largest word is wider than the rectangle, the width is expanded.。尽管文本的位置很好,但就其高度而言,它超过了原始矩形宽度。就我而言,我必须保持矩形宽度不变。有没有办法来解决这个问题?谢谢。
  • 那么你需要一个迭代的解决方案,不断尝试直到它非常适合。您可以检测到带有 DT_CALCRECT 的 DrawText 何时加宽了矩形,然后您知道您必须选择较小的字体大小并重试。
  • 可悲的是,我这篇文章的重点是避开迭代并使用数学公式来完成这项工作:(
  • 我认为你不能仅仅依靠数学,因为 (1) GDI 字体不能线性缩放,(2) 你试图同时适应水平和垂直尺寸,以及 (3 ) 自动换行使得水平和垂直之间的高度非线性关系。我会在字体大小空间中进行二进制搜索,以找到仍然使文本适合的最大大小。
【解决方案2】:

好的。基本上,我从建议的 pointSize(代码中的 14)开始,并尝试使用提供的边界矩形绘制文本。如果文本太大,我会进入一个迭代循环,减小点大小并再次测量,直到文本适合边界矩形。

另一方面,如果文本“太小”,我会进入一个循环,逐渐增加它的大小,直到它太大。一旦达到这一点,我将点大小减小 2 并返回。

减少 2 是一种混搭或破解。我注意到有时报告的大小等于或小于边界矩形的报告大小,但仍有一些字符会突出到边界矩形的边缘。

更好的解决方案是使用 DrawTextEx 函数来计算大小和绘制文本。这会更好,因为您可以使用传递给该函数的 DRAWTEXTPARAMS 结构的 iLeftmargin 和 iRightMargin 成员。无论您是希望在每一侧都有一个边距,还是只是想添加单个字符的宽度,然后在绘制文本时将其减半,这完全取决于所需的结果。我还添加了 DT_EXTERNALEADING 标志来获得文本上方/下方的小边距,尽管没有垂直填充,所以你必须利用我提到的边距属性。

由于 DT_VCENTER 标志不适用于多行文本,如果您希望文本垂直居中,您还需要自己垂直偏移文本。您只需将用于实际绘制文本的矩形偏移为区域边界矩形高度与文本边界矩形高度之间差异的一半。

我本可以在几个项目中使用这样的功能,所以感谢您实际锻炼灰质并解决问题的动力!

最后,我使用了一个交互式演示 - 一个响应(空)对话框的 WM_PAINT 消息的演示。由于 HDC 可以或多或少地被处理为打印机或屏幕,它提供了一种更快的方法来调查结果。

插入代码时的输出:(通过 cutePDF 虚拟打印机)

代码:

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

【讨论】:

  • 非常感谢您的帮助。我很惊讶以前从来没有人想到过这个想法!我一直在寻找可以替代迭代解决方案的数学公式,但这似乎是不可能的...我将尝试将其发布在 MathOverflow 上,只是为了“再试一次”(财富对那些人微笑谁在他们的坚持中忍受!)。 +1
  • 没问题。我怀疑如果您的要求是绘制单行字符串,那么计算方法会起作用,但是当需要在换行符处中断文本时,您将无法简单地按所需大小/结果大小进行缩放。 :)
  • 我已经为各种纸张尺寸和方向(横向/纵向)尝试了您的代码,似乎一切正常。在正式接受之前,我仍然会继续“战斗”一段时间以找到数学解决方案。一旦赏金期到期,我将奖励您积分(除非出现更好的解决方案)。我真的以为这会很容易,但结果却很难......
  • 酷。 :) - “我真的认为这很容易” = 著名的遗言。 :p
  • MathOverflow 上建议的数学公式在一种情况下因略微超过水平限制而失败。我打算用数据库中的数据绘制一个表格。我认为这个问题将变得比简单地使用制作“最大长度”字符串的方法->“计算”字体大小->进行绘图更复杂。太多的选择让我害怕(比如字距调整、前导和其他非线性的东西)。我很想问另一个问题,但我什至不知道我在寻找什么......“我真的认为这很容易”->不,不是->这是* ss的痛苦!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-16
  • 2023-03-27
  • 2012-03-08
  • 2015-01-16
相关资源
最近更新 更多