Рассчитайте идеальный размер шрифта на основе размера бумаги и максимально допустимой длины текста

У меня есть код для печати, который рисует сетку на бумаге.

Сетка состоит из 4 столбцов одинаковой длины по горизонтали. Высота ячейки составляет десятые доли размера бумаги. Общее количество строк неизвестно, но я точно знаю, что будет хотя бы одна строка.

Каждая ячейка имеет одинаковый физический размер -> ширина составляет четверть ширины бумаги, а высота составляет одну десятую высоты бумаги. Максимальное количество символов, которое может уместиться в ячейке, - 50.

Проблема, с которой я сталкиваюсь, заключается в выборе правильного размера шрифта, чтобы текст максимальной длины мог поместиться в ячейку.

Просматривая документацию MSDN и WinAPI примеры, я увидел, что они используют GetTextExtPoint32 для аналогичных целей, но это работает, только если шрифт уже существует и выбран в контексте устройства, что не дело здесь.

Единственное, что пришло мне в голову, это создать «фиктивный шрифт», посмотреть, поместится ли текст примера в ячейку, а затем отрегулировать его размер, если тест не пройден. Я также нашел этот блог, который рекомендует интересный подход к этому проблема, но из-за неопытности я не могу решить, «правильный ли это путь».

Вы можете порекомендовать правильное решение моей проблемы?

ИЗМЕНЕНО 30 июня 2014 г .:

Ниже приведен пример функции, которая рисует сетку и окрашивает верхнюю левую ячейку в светло-серый цвет, поскольку эта ячейка будет содержать образец текста. Таким образом мы можем визуально проверить успешность нашего кода рисования:

// 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, но было применено масштабирование из вышеуказанной функции:

введите описание изображения здесь

КОНЕЦ РЕДАКТИРОВАНИЯ

ИЗМЕНЕНО 6 июля 2014 г .:

Третье изображение сверху было результатом GDI использования значения высоты по умолчанию, потому что результат моей математической корректировки высоты шрифта был 0. Как только передается в CreateFont указанное поведение ожидается.

После правильного преобразования с double в int я получил почти идеальный результат -> последняя буква в строке едва превышает предел. Я буду продолжать попытки улучшить эту формулу, поскольку считаю ее многообещающей. Если у кого-то есть другое математическое решение, не стесняйтесь публиковать его.

КОНЕЦ РЕДАКТИРОВАНИЯ

Если требуется дополнительная информация / редактирование, оставьте комментарий, и я отвечу как можно скорее.


person AlwaysLearningNewStuff    schedule 28.06.2014    source источник
comment
Создайте фиктивный шрифт и получите длину строки максимальной длины. Не выбрасывайте его, если он не подходит - используйте его для вычисления более точного значения! Если текст имеет ширину n пикселей, а ваша ячейка - m пикселей, уменьшите размер шрифта до m / n.   -  person Jongware    schedule 28.06.2014
comment
@Jongware: Текст поместился в ячейку, но результат оказался не таким, как я ожидал. Я использую DrawTextExt для рисования тестовой строки, а тестовая строка состоит из нескольких слов. Текст должен быть выровнен по центру. Несмотря на то, что вся тестовая строка помещается в ячейку, центрирование по вертикали некорректно. При необходимости я дополню свой пост небольшим демонстрационным фрагментом. Возможно, я неправильно вас понял или неправильно применил ваш совет. С уважением.   -  person AlwaysLearningNewStuff    schedule 30.06.2014
comment
Похоже, я неверно истолковал ваше объяснение выбора правильного размера шрифта, чтобы текст максимальной длины мог поместиться в ячейку. Я не вижу упоминания о «вертикальном центрировании» в вашем сообщении, так что, возможно, вам стоит отредактировать его. (Я тоже думаю, может помочь набросок или снимок экрана того, что у вас есть.)   -  person Jongware    schedule 30.06.2014
comment
@Jongware: Думаю, вы правы, но уже поздно. Кроме того, я хотел бы действительно приложить усилия, чтобы мое редактирование было конструктивным, поэтому я сделаю обновление завтра (включая скриншоты). С наилучшими пожеланиями   -  person AlwaysLearningNewStuff    schedule 30.06.2014
comment
@Jongware: Я отредактировал свой пост в соответствии с вашими инструкциями. Что касается вертикального центрирования, я считаю, что мне поможет простая математика. Сейчас моя основная проблема отображается на последнем изображении моего редактирования. С наилучшими пожеланиями.   -  person AlwaysLearningNewStuff    schedule 30.06.2014


Ответы (2)


Здесь есть несколько проблем.

Самая большая проблема, которую я вижу, находится в этой строке:

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

Все это целые числа. В C и C ++ деление на целые числа приводит к усечению до нуля. Таким образом, если результат деления «должно» быть 3,7, вы получите 3, что может быть довольно грубым приближением.

Другая проблема заключается в том, что GetTextExtentPoint32 не переносит текст, а DrawText делает. Итак, вы измеряете текст, как будто собираетесь распечатать его как одну строку, а на самом деле рисуете как несколько строк. Вместо использования GetTextExtendPoint32 вы можете измерить высоту с помощью DrawText с помощью флага DT_CALCRECT.

Собрав их вместе, вы хотите измерить текст следующим образом:

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);
person Adrian McCarthy    schedule 30.06.2014
comment
Я пробовал ваш код, и результат не тот, который мне нужен. К сожалению, DrawTextExtDrawText тоже) расширяют ширину прямоугольника, когда длина текста больше ширины прямоугольника (см. Документацию для DT_CALCRECT): Если самое большое слово шире прямоугольника, ширина увеличивается. . Хотя текст расположен хорошо, по высоте он превышает исходную ширину прямоугольника. В моем случае я должен поддерживать постоянную ширину прямоугольника. Есть ли способ исправить это? Спасибо. - person AlwaysLearningNewStuff; 02.07.2014
comment
Тогда вам понадобится итеративное решение, которое будет продолжать попытки до тех пор, пока оно не станет подходящим. Вы можете определить, когда DrawText с DT_CALCRECT расширяет прямоугольник, а затем знаете, что вам нужно выбрать меньший размер шрифта и попробовать еще раз. - person Adrian McCarthy; 02.07.2014
comment
К сожалению, весь смысл моего поста состоял в том, чтобы избежать повторения и использовать математическую формулу для выполнения работы :( - person AlwaysLearningNewStuff; 04.07.2014
comment
Я не думаю, что вы можете полагаться только на математику, потому что (1) шрифты GDI не масштабируются линейно, (2) вы пытаетесь подогнать как по горизонтали, так и по вертикали, и (3) перенос слов не дает -линейная связь между горизонталью и вертикалью. Я бы сделал двоичный поиск в пространстве размера шрифта, чтобы найти самый большой размер, при котором текст все еще подходит. - person Adrian McCarthy; 11.07.2014

Хорошо. По сути, я начинаю с предложенного pointSize (14 в вашем коде) и пытаюсь нарисовать текст, используя предоставленный ограничивающий прямоугольник. Если текст слишком велик, я вхожу в итерационный цикл, который уменьшает размер точки и снова измеряет, пока текст не поместится в ограничивающий прямоугольник.

Если, с другой стороны, текст «слишком мал», я захожу в цикл, который постепенно увеличивает его размер, пока он не станет слишком большим. Достигнув этой точки, я уменьшаю размер точки на 2 и возвращаюсь.

Уменьшение на 2 - это путаница или взлом. Я заметил, что иногда размер сообщался как равный или меньший, чем сообщаемый размер ограничивающего прямоугольника, но все же некоторые символы выступали за край ограничивающего прямоугольника.

Лучшее решение - использовать функцию DrawTextEx как для вычисления размера, так и для рисования текста. Это было бы лучше, поскольку вы могли бы использовать элементы iLeftmargin и iRightMargin структуры DRAWTEXTPARAMS, которая передается этой функции. Независимо от того, хотите ли вы иметь поля с каждой стороны или просто хотите добавить ширину одного символа, которую вы затем уменьшили вдвое при рисовании текста, полностью зависит от желаемого результата. Я также добавил флаг DT_EXTERNALLEADING, чтобы получить небольшое поле выше / ниже текста, хотя его нет для вертикального заполнения, поэтому вам придется использовать атрибуты поля, о которых я упоминал.

Поскольку флаг 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;
}
person enhzflep    schedule 03.07.2014
comment
Большое вам спасибо за вашу помощь. Я удивлен, что до этого никто никогда не приходил в голову! Я искал математическую формулу, которая заменила бы итеративное решение, но, похоже, это невозможно ... Я постараюсь опубликовать это на MathOverflow, чтобы дать ему последнюю попытку (удача улыбается тем, кто терпит в своей настойчивости!). +1 - person AlwaysLearningNewStuff; 04.07.2014
comment
Без проблем. Я подозреваю, что если бы ваши требования заключались в том, чтобы нарисовать однострочную строку, этот расчетный подход будет работать, хотя, когда возникает необходимость разбивать текст на разрывах строки, вы не сможете просто масштабировать с помощью желаемогоSize / resultSize. :) - person enhzflep; 05.07.2014
comment
Я пробовал ваш код для разных размеров бумаги и ориентации (альбомная / портретная), и кажется, что все работает нормально. Я все еще буду бороться, чтобы найти математическое решение, прежде чем официально его приму. По истечении периода вознаграждения я награжу вас очками (если не появится лучшее решение). Я действительно думала, что это будет легко, но оказалось, что это так сложно ... - person AlwaysLearningNewStuff; 05.07.2014
comment
Прохладный. :) - Я действительно думал, что это будет легко = известные последние слова. :п - person enhzflep; 05.07.2014
comment
Математическая формула, предложенная в MathOverflow, не удалась в одном случае из-за небольшого превышения горизонтального предела. Я планировал нарисовать таблицу с данными из базы данных. Я думаю, что эта задача станет еще более сложной, чем просто использовать подход создания строки максимальной длины - ›вычислить размер шрифта -› сделать рисунок. Меня пугает слишком много вариантов (например, пары кернинга, интерлиньяж и прочие нелинейные вещи). У меня возникает соблазн задать еще один вопрос, но я даже не знаю, что ищу ... Я действительно думал, что это будет легко - ›нет, это не так -› это головная боль! - person AlwaysLearningNewStuff; 12.07.2014