GDI - Can I use the new Windows 10 Segoe UI Emoji colored font with DrawText?
Asked Answered
C

3

12

I'm creating a c++ project using Embarcadero RAD Studio (10.2 Tokyo starter) and the Windows GDI to draw text, via the DrawText() function.

I recently saw that Windows 10 provides a new "Segoe UI Emoji" font, that potentially allows text functions to draw colored emojis. I found several examples using Direct2D, but none with pure GDI functions.

I also tried a simple code, like this:

HDC hDC = ::GetDC(Handle);

std::auto_ptr<TCanvas> pCanvas(new TCanvas());
pCanvas->Handle = hDC;

pCanvas->Brush->Color = clWhite;
pCanvas->Brush->Style = bsSolid;
pCanvas->FillRect(TRect(0, 0, ClientWidth, ClientHeight));

const std::wstring text = L"Test πŸ˜€ 😬 😁 πŸ˜‚ πŸ˜ƒ πŸ˜„ πŸ˜… πŸ˜†";

TRect textRect(10, 10, ClientWidth - 10, ClientHeight - 10);

hFont = ::CreateFont(-40,
                      0, 
                      0,
                      0,
                      FW_DONTCARE,
                      FALSE,
                      FALSE,
                      FALSE,
                      DEFAULT_CHARSET,
                      OUT_OUTLINE_PRECIS,
                      CLIP_DEFAULT_PRECIS,
                      CLEARTYPE_QUALITY,
                      VARIABLE_PITCH,
                      L"Segoe UI Emoji");

::SelectObject(hDC, hFont);

::DrawTextW(hDC,
            text.c_str(),
            text.length(),
            &textRect,
            DT_LEFT | DT_TOP | DT_SINGLELINE);

::DeleteObject(hFont);

The output result sounds good in terms of symbols, but they are drawn in black&white, without colors, as you can see on the screenshot below: enter image description here

I could not find any additional options that may allow the text to be drawn using colored symbols instead of black&white. Is there a way to activate the support of the color in GDI DrawText() function, and if yes, how to do that? Or only Direct2D may draw colored emojis?

EDITED on 30.10.2017

As the GDI cannot do the job (unfortunately, and as I thought) I publish here the Direct2D version of the above code, that worked for me.

const std::wstring text = L"Test πŸ˜€ 😬 😁 πŸ˜‚ πŸ˜ƒ πŸ˜„ πŸ˜… πŸ˜†";

HDC hDC = ::GetDC(Handle);

std::auto_ptr<TCanvas> pGDICanvas(new TCanvas());
pGDICanvas->Handle = hDC;

pGDICanvas->Brush->Color = clWhite;
pGDICanvas->Brush->Style = bsSolid;
pGDICanvas->FillRect(TRect(0, 0, ClientWidth, ClientHeight));

::D2D1_RECT_F textRect;
textRect.left   = 10;
textRect.top    = 10;
textRect.right  = ClientWidth  - 10;
textRect.bottom = ClientHeight - 10;

std::auto_ptr<TDirect2DCanvas> pCanvas(new TDirect2DCanvas(hDC, TRect(0, 0, ClientWidth, ClientHeight)));

// configure Direct2D font
pCanvas->Font->Size        = 40;
pCanvas->Font->Name        = L"Segoe UI Emoji";
pCanvas->Font->Orientation = 0;
pCanvas->Font->Pitch       = System::Uitypes::TFontPitch::fpVariable;
pCanvas->Font->Style       = TFontStyles();

// get DirectWrite text format object
_di_IDWriteTextFormat pFormat = pCanvas->Font->Handle;

if (!pFormat)
    return;

pCanvas->RenderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);

::D2D1_COLOR_F color;
color.r = 0.0f;
color.g = 0.0f;
color.b = 0.0f;
color.a = 1.0f;

::ID2D1SolidColorBrush* pBrush = NULL;

// create solid color brush, use pen color if rect is completely filled with outline
pCanvas->RenderTarget->CreateSolidColorBrush(color, &pBrush);

if (!pBrush)
    return;

// set horiz alignment
pFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);

// set vert alignment
pFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);

// set reading direction
pFormat->SetReadingDirection(DWRITE_READING_DIRECTION_LEFT_TO_RIGHT);

// set word wrapping mode
pFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP);

IDWriteInlineObject* pInlineObject = NULL;

::DWRITE_TRIMMING trimming;
trimming.delimiter      = 0;
trimming.delimiterCount = 0;
trimming.granularity    = DWRITE_TRIMMING_GRANULARITY_NONE;

// set text trimming
pFormat->SetTrimming(&trimming, pInlineObject);

pCanvas->BeginDraw();

pCanvas->RenderTarget->DrawText(text.c_str(), text.length(), pFormat, textRect, pBrush,
            D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT);

pCanvas->EndDraw();

Of course this code will draw colored emojis only on the currently most recent versions of Windows 10, and above. On previous versions the text will be drawn as above (and the code may not compile).

Bonus Reading

Cory answered 23/10, 2017 at 15:15 Comment(7)
With DirectWrite, you have to opt-in to color rendering for color fonts to avoid breaking programs that weren't prepared for it. If it were possible with GDI, they'd probably also make that an opt-in. Given that DirectWrite is the only "current" text rendering stack, I don't think GDI has had an update to give you an opt-in ability. – Limacine
Thx for your answer, unfortunately it is what I thought – Cory
That's not to rule out the possibility that you could do it manually with GDI. I suppose there might be a way to instantiate the various layers of color as distinct fonts. Then you'd have to set the text color, draw the text for the current layer, and repeat for each of the colors. – Limacine
One more time thank you for your answers. In fact I already found a way to workaround the GDI to get exactly the effect I wished. However my code is complex, and I was interested to use the new Windows 10 features, that seems to do exactly the same thing, in order to not reinvent the wheel. Unfortunately it seems that the only way to achieve that is to rewrite my code to use Direct2D instead of the GDI – Cory
@AdrianMcCarthy It's interesting that GDI doesn't support it, because Unicode already supplied a way to opt-in to emoji rendering (rather than text rendering); you use the U+FE0F "selector". That lets you specify that you do indeed want the emoji. It also goes the other way, and you can use the U+FE0E selector to specify text rendering. You would think that the GDI people would see "Oh, he specified U+FE0F, he must really be ok with the emoji." ¯(°_o)/¯ – Mcdougall
@Ian Boyd: The Windows folks keep GDI working for backward compatibility. I don't think they've added new functionality in a very long time. DirectWrite is the currently support API for fancy typography. – Limacine
@AdrianMcCarthy The only downside to that is a) DirectWrite's ID2D1HwndRenderTarget.DrawText is much slower than DrawText, and b) it doesn't help existing controls like EDIT, LISTVIEW, TREEVIEW, COMBOBOX, LISTBOX, etc. When you already, by definition, have to opt-in to color fonts, it's hard to see the app-compat issue. If only someone would write a DrawText function for Windows that supported Unicode. And then Windows could use it! – Mcdougall
P
10

GDI does not support color fonts (even if you go the full Uniscribe route), you have to use Direct2D if you want color font support. It makes sense that the simpler GDI APIs don't support color fonts as color fonts require using OpenType tags and none of DrawText/TextOut provide that level of control, Uniscribe allows for such tags but has simply not been extended to support color fonts.

Potentiality answered 28/10, 2017 at 21:43 Comment(0)
J
4

You can use DirectWrite to draw colored emojis onto a bitmap in memory DC, then BitBlt() to your destination DC.

Basically, you need to implement a custom IDWriteTextRenderer class and call IDWriteTextLayout::Draw() with your renderer, then copy the result.

In your class, you retrieve IDWriteGdiInterop from IDWriteFactory and call IDWriteGdiInterop::CreateBitmapRenderTarget() to get the bitmap render target; call IDWriteFactory::CreateMonitorRenderingParams() to get the rendering parameters, and call IDWriteFactory::CreateTextFormat() to set up your text format.

The only significant method is DrawGlyphRun(), where you get IDWriteColorGlyphRunEnumerator with IDWriteFactory2::TranslateColorGlyphRun() and with each color run, call IDWriteBitmapRenderTarget::DrawGlyphRun() to do the work for you.

Just remember to update the render target/parameters when the window size/position changes.

You may reference this MSDN documentation:

Render to a GDI Surface https://msdn.microsoft.com/en-us/library/windows/desktop/ff485856(v=vs.85).aspx

Jewry answered 29/10, 2017 at 1:26 Comment(0)
R
2

As mentioned by @SoronelHaetir's answer above, the win32 graphics device interface (GDI) that is used when working with static window components (via WC_STATIC) doesn't support colorized fonts. In order to display colored emojis and/or "fancier" text (i.e. colored text, etc.), you'll need to use the Direct2D API.

The example above provided by original poster (OP) @Jean-Milost Reymond isn't something that is immediately able to be compiled and tried out by the reader. Also, it uses the TCanvas class, which isn't strictly needed when working directly with the Win32 API.

For people looking for a complete example that works on the bare-metal Win32 API and can be immediately copied and pasted and compiled, then here is the code that will compile in Visual Studio:

#define WIN32_LEAN_AND_MEAN     // Exclude rarely-used stuff from Windows headers

// Windows Header Files
#include <windows.h>

// C RunTime Header Files
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>
#include <wchar.h>
#include <math.h>

#include <d2d1.h>
#include <d2d1helper.h>
#include <dwrite.h>
#include <wincodec.h>
#include <string>
#include <cassert>

#pragma comment(lib, "d2d1.lib")
#pragma comment(lib, "Dwrite.lib")

HWND WindowHandle                       = nullptr;
IDWriteFactory * DWriteFactory          = nullptr;
ID2D1Factory * Direct2dFactory          = nullptr;
ID2D1HwndRenderTarget * RenderTarget    = nullptr;
ID2D1SolidColorBrush * TextBlackBrush   = nullptr;

const std::wstring DISPLAY_TEXT         = L"Test πŸ˜€ 😬 😁 πŸ˜‚ πŸ˜ƒ πŸ˜„ πŸ˜… πŸ˜†";

template<class Interface>
inline void SafeRelease (Interface ** ppInterfaceToRelease);
LRESULT CALLBACK WndProc (HWND hwnd,
                          UINT message,
                          WPARAM wParam,
                          LPARAM lParam);
HRESULT CreateDeviceIndependentResources ();
HRESULT InitInstance (HINSTANCE hInstance, int nCmdShow);
void DiscardDeviceResources ();
HRESULT OnRender ();
HRESULT CreateDeviceResources ();

template<class Interface>
inline void SafeRelease (Interface ** ppInterfaceToRelease)
{
    if (*ppInterfaceToRelease != NULL)
    {
        (*ppInterfaceToRelease)->Release ();

        (*ppInterfaceToRelease) = NULL;
    }
}

HRESULT OnRender ()
{
    HRESULT Result                  = S_OK;
    D2D1_SIZE_F RenderCanvasArea    = { 0 };
    IDWriteTextFormat * TextFormat  = nullptr;
    D2D1_RECT_F TextCanvasArea      = { 0 };

    Result = CreateDeviceResources ();

    if (SUCCEEDED (Result))
    {
        RenderTarget->BeginDraw ();

        RenderCanvasArea = RenderTarget->GetSize ();

        RenderTarget->Clear (D2D1::ColorF (D2D1::ColorF::White));

        if (SUCCEEDED (Result))
        {
            Result = DWriteFactory->CreateTextFormat (L"Segoe UI",
                                                      nullptr,
                                                      DWRITE_FONT_WEIGHT_REGULAR,
                                                      DWRITE_FONT_STYLE_NORMAL,
                                                      DWRITE_FONT_STRETCH_NORMAL,
                                                      25.0f,
                                                      L"en-us",
                                                      &TextFormat);

            TextFormat->SetTextAlignment (DWRITE_TEXT_ALIGNMENT_LEADING);
            TextFormat->SetParagraphAlignment (DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
            TextFormat->SetReadingDirection (DWRITE_READING_DIRECTION_LEFT_TO_RIGHT);
            TextFormat->SetWordWrapping (DWRITE_WORD_WRAPPING_WRAP);

            if (SUCCEEDED (Result) &&
                TextFormat != nullptr)
            {
                TextCanvasArea = D2D1::RectF (0,
                                              0,
                                              RenderCanvasArea.width,
                                              RenderCanvasArea.height);

                RenderTarget->DrawTextW (DISPLAY_TEXT.c_str (),
                                         static_cast <UINT32> (DISPLAY_TEXT.size ()),
                                         TextFormat,
                                         TextCanvasArea,
                                         TextBlackBrush,
                                         D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT);
            }
        }

        Result = RenderTarget->EndDraw ();
    }

    if (Result == D2DERR_RECREATE_TARGET)
    {
        DiscardDeviceResources ();

        Result = S_OK;
    }

    return Result;
}

HRESULT CreateDeviceResources ()
{
    HRESULT Result  = S_OK;
    RECT rc         = { 0 };

    if (!RenderTarget)
    {
        GetClientRect (WindowHandle,
                       &rc);

        D2D1_SIZE_U size = D2D1::SizeU (rc.right - rc.left,
                                        rc.bottom - rc.top);

        // Create a Direct2D render target.
        Result = Direct2dFactory->CreateHwndRenderTarget (D2D1::RenderTargetProperties (),
                                                          D2D1::HwndRenderTargetProperties (WindowHandle, size),
                                                          &RenderTarget);

        if (SUCCEEDED (Result))
        {
            // Create a blue brush.
            Result = RenderTarget->CreateSolidColorBrush (D2D1::ColorF (D2D1::ColorF::Black),
                                                          &TextBlackBrush);
        }
    }

    return Result;
}

void DiscardDeviceResources ()
{
    SafeRelease (&RenderTarget);
    SafeRelease (&TextBlackBrush);
}

HRESULT InitInstance (HINSTANCE hInstance,
                      int nCmdShow)
{
    HRESULT Result = S_OK;

    // Create the window.
    WindowHandle = CreateWindow (L"D2DTextDemo",
                                 L"Direct2D Text Demo Application",
                                 WS_OVERLAPPEDWINDOW,
                                 CW_USEDEFAULT,
                                 CW_USEDEFAULT,
                                 600,
                                 200,
                                 nullptr,
                                 nullptr,
                                 hInstance,
                                 nullptr);

    if (WindowHandle == nullptr)
    {
        Result = E_POINTER;
    }
    else
    {
        ShowWindow (WindowHandle,
                    nCmdShow);
        UpdateWindow (WindowHandle);
    }

    return Result;
}

HRESULT CreateDeviceIndependentResources ()
{
    HRESULT Result = S_OK;

    Result = D2D1CreateFactory (D2D1_FACTORY_TYPE_SINGLE_THREADED,
                                &Direct2dFactory);

    if (SUCCEEDED (Result))
    {
        Result = DWriteCreateFactory (DWRITE_FACTORY_TYPE_SHARED,
                                      __uuidof (IDWriteFactory),
                                      reinterpret_cast <IUnknown **> (&DWriteFactory));
    }

    return Result;
}

LRESULT CALLBACK WndProc (HWND hwnd,
                          UINT message,
                          WPARAM wParam,
                          LPARAM lParam)
{
    LRESULT Result = 0;

    switch (message)
    {
        case WM_SIZE:
        {
            UINT width = LOWORD (lParam);
            UINT height = HIWORD (lParam);

            if (RenderTarget != nullptr)
            {
                // Note: This method can fail, but it's okay to ignore the
                // error here, because the error will be returned again
                // the next time EndDraw is called.
                RenderTarget->Resize (D2D1::SizeU (width,
                                                    height));
            }
        }
        break;

        case WM_DISPLAYCHANGE:
        {
            InvalidateRect (hwnd, nullptr, FALSE);
        }
        break;

        case WM_PAINT:
        {
            OnRender ();
            ValidateRect (hwnd,
                            nullptr);
        }
        break;

        case WM_DESTROY:
        {
            PostQuitMessage (0);
            Result = 1;
        }
        break;

        default:
        {
            Result = DefWindowProc (hwnd,
                                    message,
                                    wParam,
                                    lParam);
        }
        break;
    }

    return Result;
}

int APIENTRY wWinMain (_In_ HINSTANCE hInstance,
                       _In_opt_ HINSTANCE hPrevInstance,
                       _In_ LPWSTR lpCmdLine,
                       _In_ int nCmdShow)
{
    UNREFERENCED_PARAMETER (hInstance);
    UNREFERENCED_PARAMETER (hPrevInstance);
    UNREFERENCED_PARAMETER (lpCmdLine);
    UNREFERENCED_PARAMETER (nCmdShow);

    HRESULT ExitCode    = S_OK;
    MSG NextMessage     = { 0 };
    WNDCLASSEX wcex     = { 0 };
    ATOM WindowClassId  = 0;

    wcex.cbSize         = sizeof (WNDCLASSEX);
    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = sizeof (LONG_PTR);
    wcex.hInstance      = hInstance;
    wcex.hbrBackground  = nullptr;
    wcex.lpszMenuName   = nullptr;
    wcex.hCursor        = LoadCursor (nullptr, IDI_APPLICATION);
    wcex.lpszClassName  = L"D2DTextDemo";

    if (SUCCEEDED (CoInitialize (nullptr)))
    {
        WindowClassId = RegisterClassEx (&wcex);

        if (WindowClassId == 0)
        {
            ExitCode = HRESULT_FROM_WIN32 (GetLastError ());
        }

        if (SUCCEEDED (ExitCode))
        {
            ExitCode = CreateDeviceIndependentResources ();
        }

        if (SUCCEEDED (ExitCode))
        {
            ExitCode = InitInstance (hInstance,
                                     nCmdShow);
        }

        if (SUCCEEDED (ExitCode))
        {
            while (GetMessage (&NextMessage,
                                nullptr,
                                0,
                                0))
            {
                TranslateMessage (&NextMessage);
                DispatchMessage (&NextMessage);
            }
        }

        CoUninitialize ();

        SafeRelease (&Direct2dFactory);
        SafeRelease (&DWriteFactory);
        SafeRelease (&RenderTarget);
    }

    return ExitCode;
}

(The above example doesn't have perfect error handling, so it's important to audit this example code if the following is used in any project the reader is working on.)

Before attempting to compile this in Visual Studio, make sure your project has the "SubSystem" linker option set to Windows /SUBSYSTEM:WINDOWS. Windows Subsystem Linker Option in Visual Studio

Once compiled successfully, the following application window will appear: The end result of compiling and executing the example code.

I tested this coded example in Visual Studio 2022 Community Edition on Windows 11 with success.

Reference(s):

Robinrobina answered 27/3, 2022 at 22:26 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.