Jump to content
  • Advertisement
  • entries
  • comments
  • views

Sweet Snippets - More Text Rendering with DirectWrite/Direct2D and Direct3D11.

Sign in to follow this  


Previously we built ourselves a short little application that displayed some text over a triangle. Rendered using Direct3D11, DirectWrite and Direct2D. There were a few problems with the sample though, and so I've decided to do a followup which shows some changes which fix those issues.


When you initially get text rendering with Direct2D, using DrawText and DirectWrite, it feels rather powerful at first. You're able to render text, with a brush of your choosing, to a texture or the screen. But you will quickly find that DrawText is actually not that great of a function. Hence we have the IDWriteTextLayout interface.

This interface allows us the capability to build much more complex text objects, and in fact is used internally by DrawText. The interface provides a great deal of functionality, and so we shall now harness it to enhance the previous example.

But first, we need a goal. Goals are important in every field, including software development. Without an end goal in mind, code quickly begins to wander, and you soon find yourself in dark alleys best not trod. Thus our goal: To be able to render text that includes hyperlinks. These links will render in a fixed width font, with a different color, and when the mouse moves over them we expect our cursor to change from an arrow to a hand. Furthermore, when we click a link we expect it to open the default browser to the URL the link points to.

Using IDWriteTextLayout

The IDWriteTextLayout interface is fairly simple, you construct it by providing it with the text you desire to layout, the bounds of the text, and a default formatter (which provides font information).
auto result = m_factory->CreateTextLayout(m_text.c_str(), m_text.length(), m_defaultFormat, size.x, size.y, &m_textLayout);As you can see from the snippet above, it's quite trivial to use. But this does raise the question? How does this help us to format our text with links? Well, in our sample we are actually building m_text from smaller bits of text. We use two functions, AppendText and AppendLink to fill the m_text string. However, each time we call AppendLink we also store a few other bits of information: The starting position of the link, and the length, along with the URL associated with this range. As can be seen below:
void AppendText(tstring const & str) { m_text.append(str); m_dirty = true;}void AppendLink(tstring const & str, tstring const & link) { DWRITE_TEXT_RANGE linkRange = { m_text.size(), str.length() }; m_linkRanges.push_back(std::make_pair(linkRange, link)); m_text.append(str); m_dirty = true;}We also set a dirty flag, which we use to determine if the text layout object needs to be recreated.

Once we've built up our text we "compile" it into a text layout object. Once we have our IDWriteTextLayout object created with our text, we need to tell it how to format the text to our liking. In our case, we need to tell it about the links in our text and how we desire to have them rendered.

To do this, we simply iterate over the previously saved ranges (from AppendLink) and tell the text layout interface that for those ranges of characters we desire them to be drawn differently. In our case we're going to render them as being a fixed width font (Consolas), underlined, and a nice powdered blue color:
for (auto const & p : m_linkRanges) { m_textLayout->SetFontFamilyName(_T("Consolas"), p.first); m_textLayout->SetUnderline(true, p.first); m_textLayout->SetDrawingEffect(m_linkBrush, p.first);}.

Drawing the Text and More

Drawing couldn't be simpler. In fact, it's actually simpler than drawing text using DrawText. Since we've done all the work up front to format our text, all we really have to do is pass it off to Direct2D, along with where on the screen (or texture) we desire to render it.
renderTarget->DrawTextLayout(pos, m_textLayout, m_defaultBrush);Of course, this is not where we're going to stop, obviously. Now that we have our text rendering nicely to the screen, we obviously now want to be able to detect if the user has their mouse over the links in our text. To do this we need to perform two actions: The first is that we need to trap the WM_MOUSEMOVE and WM_LBUTTONUP Win32 mouse events, the second is that we need some way to detect where the cursor is in relation to our various links. Thankfully, DirectWrite helps out here too!

Since DirectWrite is a text layout engine, it can tell us a lot about the text it's laying out. Including such things as "where is a point in relation to the characters of this IDWriteTextLayout object." The test is quite trivial:
m_textLayout->HitTestPoint(pos.x, pos.y, &isTrailingHit, &isInside, &hitTestMetrics);With the returned booleans from this function we know if it's hitting the trailing edge of a character, if it's inside the text area at all, and several other metrics from the hit test as well. In our case we will be using the isInside boolean to determine if we should be continuing our tests further, and then from the DWRITE_HIT_TEST_METRICS we'll be using the textPosition member to determine the nearest character to the cursor. WIth that information in hand it's a simple task to iterate over our links (that we stored previously from AppendLink) and check if the textPosition is within the range of characters represented by the link:
for (auto const & p : m_linkRanges) { if (hitTestMetrics.textPosition >= p.first.startPosition && hitTestMetrics.textPosition < p.first.startPosition + p.first.length) { *linkText = p.second; return true; }}We can then use the information from the hit test to perform various actions, such as using ShellExecute to open the browser to the link location:
LRESULT OnMouseUp(unsigned msg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { tstring link; if (m_textSection->IsOverLink(D2D1::Point2F((float)GET_X_LPARAM(lParam), (float)GET_Y_LPARAM(lParam)), &link)) { ShellExecute(NULL, _T("open"), link.c_str(), NULL, NULL, SW_SHOWNORMAL); } return 0;}The rest of what you can do is really only limited by your imagination.

Full Sample

#define NOMINMAX#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "d3d11.lib")#pragma comment(lib, "d2d1.lib")#pragma comment(lib, "dwrite.lib")#pragma comment(lib, "d3dcompiler.lib")#ifdef UNICODEtypedef std::wstring tstring;typedef wchar_t tchar;#elsetypedef std::string tstring;typedef char tchar;#endifstruct Vertex { float position[4]; float color[4]; float texCoord[2];};class Exception : public std::runtime_error {public: Exception(std::string const & error, HRESULT result) : std::runtime_error(error + "\nError was: " + std::to_string(result)) { }};class TextSection { typedef std::pair LinkPair;public: TextSection(CComPtr factory) : m_factory(factory), m_dirty(true) { auto result = m_factory->CreateTextFormat(_T("Calibri"), nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, 14.0f, _T(""), &m_defaultFormat); if (FAILED(result)) throw Exception("Failed to create text format.", result); } void SetDefaultColorBrush(CComPtr defaultBrush) { m_defaultBrush = defaultBrush; } void SetLinkColorBrush(CComPtr linkBrush) { m_linkBrush = linkBrush; } void AppendText(tstring const & str) { m_text.append(str); m_dirty = true; } void AppendLink(tstring const & str, tstring const & link) { DWRITE_TEXT_RANGE linkRange = { m_text.size(), str.length() }; m_linkRanges.push_back(std::make_pair(linkRange, link)); m_text.append(str); m_dirty = true; } void Compile(D2D1_POINT_2F const & size) { if (!m_defaultBrush || !m_linkBrush) { throw Exception("Default and link color brushes must be set first.", E_FAIL); } if (m_textLayout) { m_textLayout.Release(); } auto result = m_factory->CreateTextLayout(m_text.c_str(), m_text.length(), m_defaultFormat, size.x, size.y, &m_textLayout); if (FAILED(result)) { throw Exception("Unable to create text layout.", result); } for (auto const & p : m_linkRanges) { m_textLayout->SetFontFamilyName(_T("Consolas"), p.first); m_textLayout->SetUnderline(true, p.first); m_textLayout->SetDrawingEffect(m_linkBrush, p.first); } m_dirty = false; } void Release() { m_defaultBrush.Release(); m_linkBrush.Release(); m_textLayout.Release(); } void Draw(CComPtr renderTarget, D2D1_POINT_2F const & pos) { if (m_dirty || !m_linkBrush || !m_defaultBrush || !m_textLayout) { return; } renderTarget->DrawTextLayout(pos, m_textLayout, m_defaultBrush); } bool IsOverLink(D2D1_POINT_2F const & pos, tstring * linkText) { BOOL isTrailingHit; BOOL isInside; DWRITE_HIT_TEST_METRICS hitTestMetrics; m_textLayout->HitTestPoint(pos.x, pos.y, &isTrailingHit, &isInside, &hitTestMetrics); if (!isInside) return false; for (auto const & p : m_linkRanges) { if (hitTestMetrics.textPosition >= p.first.startPosition && hitTestMetrics.textPosition < p.first.startPosition + p.first.length) { if (linkText != nullptr) { *linkText = p.second; } return true; } } return false; }private: CComPtr m_factory; CComPtr m_defaultFormat; CComPtr m_textLayout; CComPtr m_defaultBrush; CComPtr m_linkBrush; tstring m_text; std::vector m_linkRanges; bool m_dirty;};class MainWindow : public CWindowImpl, public CIdleHandler {public: MainWindow() { m_handCursor = LoadCursor(nullptr, IDC_HAND); m_arrowCursor = LoadCursor(nullptr, IDC_ARROW); RECT bounds = { 0, 0, 800, 600 }; AdjustWindowRect(&bounds, WS_OVERLAPPEDWINDOW, false); bounds = { 0, 0, bounds.right - bounds.left, bounds.bottom - bounds.top }; Create(nullptr, bounds, _T("D3DSample Window"), WS_OVERLAPPEDWINDOW); ShowWindow(SW_SHOW); } virtual BOOL OnIdle() { Present(); return true; } void Present() { static float clearColor[] = { 0, 0, 0, 1 }; { m_deviceContext->OMSetRenderTargets(1, &m_backBufferRTV.p, nullptr); m_deviceContext->ClearRenderTargetView(m_backBufferRTV, clearColor); size_t stride = sizeof(Vertex); size_t offsets = 0; m_deviceContext->IASetVertexBuffers(0, 1, &m_vertexBuffer.p, &stride, &offsets); m_deviceContext->IASetInputLayout(m_inputLayout); m_deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); m_deviceContext->VSSetShader(m_vertexShader, nullptr, 0); m_deviceContext->PSSetShader(m_pixelShader, nullptr, 0); } { m_deviceContext->Draw(3, 0); } { m_d2dRenderTarget->BeginDraw(); m_textSection->Draw(m_d2dRenderTarget, D2D1::Point2F(0, 0)); m_d2dRenderTarget->EndDraw(); } { m_swapChain->Present(0, 0); } }public: BEGIN_MSG_MAP(MainWindow) MESSAGE_HANDLER(WM_DESTROY, [](unsigned msg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { PostQuitMessage(0); return 0; }); MESSAGE_HANDLER(WM_SIZE, OnSize); MESSAGE_HANDLER(WM_CREATE, OnCreate); MESSAGE_HANDLER(WM_MOUSEMOVE, OnMouseMove); MESSAGE_HANDLER(WM_LBUTTONUP, OnMouseUp); END_MSG_MAP()private: void CreateD3DVertexAndShaders() { tstring processFilename(MAX_PATH, _T('\0')); std::vector vertexShader; std::vector pixelShader; GetModuleFileName(GetModuleHandle(nullptr), &processFilename.front(), processFilename.length()); SetCurrentDirectory(processFilename.substr(0, processFilename.find_last_of(_T("\\/"))).data()); { std::ifstream vertexfin("VertexShader.cso", std::ios_base::binary | std::ios_base::in); std::copy(std::istreambuf_iterator(vertexfin), std::istreambuf_iterator(), std::back_inserter(vertexShader)); auto result = m_device->CreateVertexShader(&vertexShader.front(), vertexShader.size(), nullptr, &m_vertexShader); if (FAILED(result)) { throw Exception("Failed to create vertex shader.", result); } } { std::ifstream pixelfin("PixelShader.cso", std::ios_base::binary | std::ios_base::in); std::copy(std::istreambuf_iterator(pixelfin), std::istreambuf_iterator(), std::back_inserter(pixelShader)); auto result = m_device->CreatePixelShader(&pixelShader.front(), pixelShader.size(), nullptr, &m_pixelShader); if (FAILED(result)) { throw Exception("Failed to create pixel shader.", result); } } CComPtr inputLayoutBlob; auto result = D3DGetInputSignatureBlob(&vertexShader.front(), vertexShader.size(), &inputLayoutBlob); if (FAILED(result)) { throw Exception("Failed to get input layout.", result); } // Hard coded triangle. Tis a silly idea, but works for the sample. Vertex vertices[] = { { { 0.0f, 0.5f, 0.5f, 1.0f }, { 1.0f, 0.0f, 0.0f, 1.0f }, { 0.5f, 1.0f } }, { { 0.5f, -0.5f, 0.5f, 1.0f }, { 0.0f, 1.0f, 0.0f, 1.0f }, { 0.0f, 0.0f } }, { { -0.5f, -0.5f, 0.5f, 1.0f }, { 0.0f, 0.0f, 1.0f, 1.0f }, { 1.0f, 0.0f } }, }; D3D11_BUFFER_DESC desc = { sizeof(vertices), D3D11_USAGE_DEFAULT, D3D11_BIND_VERTEX_BUFFER }; D3D11_SUBRESOURCE_DATA data = { vertices }; result = m_device->CreateBuffer(&desc, &data, &m_vertexBuffer); if (FAILED(result)) { throw Exception("Failed to create vertex buffer.", result); } D3D11_INPUT_ELEMENT_DESC inputElementDesc[] = { { "SV_POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0 }, { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 4 * sizeof(float) }, { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 4 * sizeof(float) }, }; result = m_device->CreateInputLayout(inputElementDesc, sizeof(inputElementDesc) / sizeof(D3D11_INPUT_ELEMENT_DESC), inputLayoutBlob->GetBufferPointer(), inputLayoutBlob->GetBufferSize(), &m_inputLayout); if (FAILED(result)) { throw Exception("Failed to create input layout.", result); } } void CreateD3DResources() { D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, }; // We only want to draw to the portion of the window that is the client rect. // This will also work for dialog / borderless windows. RECT clientRect; GetClientRect(&clientRect); DXGI_SWAP_CHAIN_DESC swapChainDesc = { { clientRect.right, clientRect.bottom, { 60, 1 }, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED, DXGI_MODE_SCALING_UNSPECIFIED }, { 1, 0 }, DXGI_USAGE_BACK_BUFFER | DXGI_USAGE_RENDER_TARGET_OUTPUT, 1, m_hWnd, true, DXGI_SWAP_EFFECT_DISCARD, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH }; // At the moment we don't actually care about what feature level we got back, so we don't keep this around just yet. D3D_FEATURE_LEVEL featureLevel; auto result = D3D11CreateDeviceAndSwapChain( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, // BGRA Support is necessary for D2D functionality. D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG, // D2D works with all of our feature levels, so we don't actually care which oen we get. featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION, &swapChainDesc, &m_swapChain, &m_device, &featureLevel, &m_deviceContext ); if (FAILED(result)) { throw Exception("Failed to create D3D device and DXGI swap chain.", result); } // And lets create our D2D factory and DWrite factory at this point as well, that way if any of them fail we'll fail out completely. auto options = D2D1_FACTORY_OPTIONS(); options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; result = D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, options, &m_d2dFactory); if (FAILED(result)) { throw Exception("Failed to create multithreaded D2D factory.", result); } result = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(&m_dwFactory)); if (FAILED(result)) { throw Exception("Failed to create DirectWrite Factory.", result); } } void CreateBackBufferTarget() { CComPtr backBuffer; // Get a pointer to our back buffer texture. auto result = m_swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)); if (FAILED(result)) { throw Exception("Failed to get back buffer.", result); } // We acquire a render target view to the entire surface (no parameters), with nothing special about it. result = m_device->CreateRenderTargetView(backBuffer, nullptr, &m_backBufferRTV); if (FAILED(result)) { throw Exception("Failed to create render target view for back buffer.", result); } } void CreateD2DResources() { CComPtr bufferSurface; // Get a DXGI surface for D2D use. auto result = m_swapChain->GetBuffer(0, IID_PPV_ARGS(&bufferSurface)); if (FAILED(result)) { throw Exception("Failed to get DXGI surface for back buffer.", result); } // Proper DPI support is very important. Most applications do stupid things like hard coding this, which is why you, // can't use proper DPI on most monitors in Windows yet. float dpiX; float dpiY; m_d2dFactory->GetDesktopDpi(&dpiX, &dpiY); // DXGI_FORMAT_UNKNOWN will cause it to use the same format as the back buffer (R8G8B8A8_UNORM) auto d2dRTProps = D2D1::RenderTargetProperties(D2D1_RENDER_TARGET_TYPE_DEFAULT, D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED), dpiX, dpiY); // Wraps up our DXGI surface in a D2D render target. result = m_d2dFactory->CreateDxgiSurfaceRenderTarget(bufferSurface, &d2dRTProps, &m_d2dRenderTarget); if (FAILED(result)) { throw Exception("Failed to create D2D DXGI Render Target.", result); } result = m_d2dRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &m_defaultColorBrush); if (FAILED(result)) { throw Exception("Failed to create D2D color brush.", result); } result = m_d2dRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::PowderBlue), &m_linkColorBrush); if (FAILED(result)) { throw Exception("Failed to create D2D color brush.", result); } } void CreateTextResources() { m_textSection = std::make_unique(m_dwFactory); m_textSection->SetDefaultColorBrush(m_defaultColorBrush); m_textSection->SetLinkColorBrush(m_linkColorBrush); m_textSection->AppendText(_T("Tutorials are a horrible way to learn.\n\nI've covered that before though, and so have others, so I won't go into a great deal of depth on the subject, but suffice it to say that tutorials don't have the depth nor breadth to cover a subject in any sufficient detail to be terribly useful. If you don't learn to program, and if you don't learn to learn, then you'll always be stuck in ruts like this...\n\nThat being said, I have written a ")); m_textSection->AppendLink(_T("sweet little snippet to demonstrate exactly how to render text to the screen using Direct2D"), _T("http://www.gamedev.net/blog/32/entry-2260628-sweet-snippets-rendering-text-with-directwritedirect2d-and-direct3d11/")); m_textSection->AppendText(_T(".")); RECT clientRect;; GetClientRect(&clientRect); m_textSection->Compile(D2D1::Point2F(clientRect.right / 2.0f, clientRect.bottom / 2.0f)); }private: LRESULT OnMouseUp(unsigned msg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { tstring link; if (m_textSection->IsOverLink(D2D1::Point2F((float)GET_X_LPARAM(lParam), (float)GET_Y_LPARAM(lParam)), &link)) { ShellExecute(NULL, _T("open"), link.c_str(), NULL, NULL, SW_SHOWNORMAL); } return 0; } LRESULT OnMouseMove(unsigned msg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { if (m_textSection->IsOverLink(D2D1::Point2F((float)GET_X_LPARAM(lParam), (float)GET_Y_LPARAM(lParam)), nullptr)) { SetCursor(m_handCursor); } else { SetCursor(m_arrowCursor); } return 0; } LRESULT OnSize(unsigned msg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { // We need to release everything that may be holding a reference to the back buffer. // This includes D2D interfaces as well, as they hold a reference to the DXGI surface. m_textSection->Release(); m_linkColorBrush.Release(); m_defaultColorBrush.Release(); m_d2dRenderTarget.Release(); m_backBufferRTV.Release(); // And we make sure that we do not have any render tarvets bound either, which could // also be holding references to the back buffer. m_deviceContext->ClearState(); int width = LOWORD(lParam); int height = HIWORD(lParam); auto result = m_swapChain->ResizeBuffers(1, width, height, DXGI_FORMAT_UNKNOWN, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH); if (FAILED(result)) { std::cout << "Failed to resize swap chain." << std::endl; std::cout << "Error was: " << std::hex << result << std::endl; return -1; } try { // We need to recreate those resources we disposed of above, including our D2D interfaces CreateBackBufferTarget(); CreateD2DResources(); m_textSection->SetDefaultColorBrush(m_defaultColorBrush); m_textSection->SetLinkColorBrush(m_linkColorBrush); m_textSection->Compile(D2D1::Point2F(width / 2.0f, height / 2.0f)); } catch (Exception & ex) { std::cout << ex.what() << std::endl; } D3D11_VIEWPORT viewport = { 0.0f, 0.0f, static_cast(width), static_cast(height), 0.0f, 1.0f }; // We setup our viewport here as the size of the viewport is known at this point, WM_SIZE will be sent after a WM_CREATE. m_deviceContext->RSSetViewports(1, &viewport); return 0; } LRESULT OnCreate(unsigned msg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { try { CreateD3DResources(); CreateBackBufferTarget(); CreateD3DVertexAndShaders(); CreateD2DResources(); CreateTextResources(); } catch (Exception & ex) { std::cout << ex.what() << std::endl; return -1; } return 0; }private: CComPtr m_swapChain; CComPtr m_device; CComPtr m_deviceContext; CComPtr m_backBufferRTV; CComPtr m_vertexBuffer; CComPtr m_inputLayout; CComPtr m_vertexShader; CComPtr m_pixelShader; CComPtr m_d2dFactory; CComPtr m_d2dRenderTarget; CComPtr m_defaultColorBrush; CComPtr m_linkColorBrush; CComPtr m_dwFactory; CComPtr m_dwFormat; std::unique_ptr m_textSection; HCURSOR m_handCursor; HCURSOR m_arrowCursor;};int main() { CAppModule appModule; CMessageLoop messageLoop; MainWindow window; appModule.Init(nullptr, GetModuleHandle(nullptr)); appModule.AddMessageLoop(&messageLoop); messageLoop.AddIdleHandler(&window); messageLoop.Run(); appModule.Term(); return 0;}
Sign in to follow this  

1 Comment

Recommended Comments

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!