JavaScriptを有効にすると、ここにメニューを表示します。
UWPのOpenGL ES 2.0プログラムでシステムフォントを使ってテキストを表示する

ysflight.com

[サンプルコードのダウンロード]

 

2017/09/09 CMake 3.8で.targetsファイルをライブラリとして認識するようになったので、それに対応。

 

ここで、CMakeを使って、OpenGL ES2.0を使ったUniversal Windows Template用アプリをコンパイルする方法を書いた。

次のステップとして、システムフォントを使って文字をレンダリングするライブラリを移植しようと思ったのだが、ここで壁にぶちあたってしまった。"Universal Windows Platform render text"みたいなキーワードで検索すると、最初にヒットするのが、TextBlockクラス。これを使えばいいのか、と、思ったら、ランタイムエラーが出る。

The application called an interface that was marshalled for a different thread.

さらに調べると、このエラーは、なんでもUIスレッドから使うべきクラスをメインスレッドから使うとき出る、とのこと。テキストレンダリングがUIスレッドからしかできないのも変な話だ、と、思ったが、RunAsyncというのを使えばできる、とのことで、試してみるが、これもだめ。同じエラーが出る。

こういうエラーに直面したとき、Stackoverflowとかで、質問だけ放置されていて、解決につながる回答がポストされていない場合、ほぼ絶望的と言える。多分、解決は無い。この、TextBlockというクラス、Xamlというフレームワークを使っている場合のみUIスレッドから使用可能なクラスのようだ。

Xamlは、多分、よく見るページをめくって何かするようなアプリケーションに向いている。しかし、すべてのアプリケーションに向いているわけではない。また、OpenGL ES 2.0のテンプレートだけでなく、DirectX11のテンプレートも、Xamlを使ったものと使っていないものの両方が提供されていて、DirectX11のテンプレートにいたっては、XamlとDirectX11を組み合わせると、パフォーマンスが落ちる、とMSDNに書いてある。("but using the XAML template may result in lower performance.") Xamlを使う上で、僕が一番困るのは、このXamlファイルの得体が知れないことと、Windowsでしか使えないということだ。とにかく、Windowsでしか使えないリソースをできるだけ排除したい。ついでに、中身がよくわからないから、この先仕様が変わったから使えなくなりました、と言われたとき、どう直していいのかわからない。今の僕に、Windowsでしか使えないような言語を新たに勉強している時間は無い。

同じ問題で困ってる人はほかにもいるようで、探せば探すほど質問だけ見つかるが、答えが無い。だんだん絶望的になってきた、のだが、偶然、何のキーワードで検索したときだったか、割と検索順位下の方に、Win2Dなるものを見つける。Win2Dは、マイクロソフトが正式にサポートしているクラスライブラリだから、割と安心して使える。そして、まさに、僕が探してたような用途に使えるライブラリだった。Win2Dを使うと、簡単にオフスクリーンレンダリングができて、例えば、テキストメッセージをシステムフォントでテクスチャとしてレンダリングしたいような場合に使える。そして、Xamlに依存しない。

リターン・オブ・NuGet対CMake問題

Win2Dは、なぜかVisual Studio 2015のデフォルトインストールパッケージに入ってこない。だから、あとからダウンロードしてプロジェクトに依存関係を設定しなくてはならない。Visual Studio内でやるなら、簡単にGUIからできるが、僕の場合、CMakeからできないと意味がない。

NuGetパッケージ、ANGLE.WindowsStoreに関しては、CMakeLists.txt内から、.lib, .h, .dllファイルを見つけて、add_libraryでインポートライブラリとして認識させて、target_include_directoryでヘッダパスを追加して、DLLファイルは後からAppXディレクトリにコピーできるように、変数にファイル名を入れて対応できた。

が、Win2Dパッケージは、.winmdとかいう形式のファイルが、libディレクトリ内に入っている。手も足も出ない。これは、直接.vcxprojを書き換えるしかない。どうせなら、ANGLE.WindowsStoreも同じように.vcxproj内に直接指定してしまえば、CMakeLists.txtを簡潔に書ける。

ANGLE.WindowsStoreを追加したときわかったことは、.vcxprojファイルの最後の</Project>タグの前に、

<Import Project="(full-path file name for .targets file)" />

と記述すれば、NuGetパッケージへの依存関係を追加することができた。このタグだけで、ヘッダのパス、ライブラリファイル、それにAppXパッケージにコピーする必要のあるDLLまで全自動で認識される。

ちなみに、CMake 3.6.2のソースコードを見て、今のCMakeではこの.targetsファイルへの依存関係を記述できなさそう、と、確認した。ついでに、ちょっとソースをいじくってみたら、非常に小さな変更で、.targetsファイルに対応できることも確認した。CMake.orgにフィーチャーリクエストを出してみたけど、対応してくれるかなあ。小さな変更で対応できることは実証したけど、その変更が正しい変更かどうかという問題があるからね。下手な小さな変更をそのまま入れてしまうと、先々のバージョンでにっちもさっちも行かなくなることがあるから、CMakeの人たちが不適当と判断したらそのときはしょうがない。

→ そして、結局その部分のコードは僕が書くことになり、珍しく僕らしくなくオープンソースコミュニティにコードを提供してしまった。

CMake 3.8から、.targetsファイルをライブラリとして記述できることになった。だから、以前よりずいぶん楽にNuGetで提供されるライブラリをCMakeから使えるようになった。使い方は、単にtarget_link_librariesで、普通のライブラリの様に.targetsファイルを指定するだけ。もちろん、Visual Studioでコンパイルしている場合のみだから、クロスプラットフォームに書いている人は場合分けする。

Win2DとOpenGL ESで文字を描く

今回何がしたかったかというと、OpenGL ES 2.0で、システムフォントで文字を描画できるようにしたかった。オフスクリーンビットマップにシステムフォントで字が描ければいい。オフスクリーンビットマップができれば、あとはそれをテクスチャにして描画すればいい。元祖のOpenGLだったら、glDrawPixelsを使って一発で描画できるけど、OpenGL ES 2.0にはその便利な関数は無い。ただ、ここではOpenGL ESの説明は書かないし、自分でもわざわざ例題用にシェーダーを書き直すのが面倒だったんで、自作ライブラリのYsGLSLRenderRGBABitmap2Dを使った。この中では、単にRGBAビットマップをテクスチャにして1:1の大きさでGL_TRIANGLE_FANとして描画してるだけだから、多分、OpenGL ES 2.0がわかってる人なら何が起きてるかわかると思う。

オフスクリーンビットマップを作ってるのは、std::vector SingleWindowAppView::RenderTextToBitmap(int &w,int &h,const char msg[]) 。描画してるのは、Run()の中の、YsGLSLRenderRGBABitmap2D。このコードでは、毎フレームごとにビットマップを作って、テクスチャを登録して、ビットマップを削除して、を繰り返しているけど、現実的なアプリケーションでは、ビットマップはテクスチャとしてキャッシュした方がいい。せめて、ビットマップをキャッシュするだけでも、毎回作り直すより良くなるはず。

OpenGL ES用のオフスクリーンビットマップを作るとき、注意する点は、CanvasRenderingTargetのビットマップは、BGRA、OpenGLのビットマップは、RGBAであるということ。順を入れ替えないと変な色になる。8ビット時代にVRAMに書き込むアドレスを間違えたときみたいな。それから、WindowsのビットマップとOpenGLのビットマップは上下が逆だから、ひっくり返す必要がある。

また、cs->Flush();を呼ぶまで一切の描画は起きないから、これを忘れると、何も出ない。このことに気づかなくて、数時間無駄にした。何をやっても字が出ない。ひょっとしてビットマップ範囲外に書いているのではないかと場所を変えてみても何も出ない。ひょっとして、背景と同じ色で描いてしまっているのかと思って、ウィンドウの背景を変え、ビットマップの背景を変え、字の色を変え、それでも何も出ない。それどころか、背景をグリーンにクリアしてるのに、背景の色もグリーンにならない。う〜ん、何が起きているのだ?まさかWin2DもXamlが無いと動かないのか?ひょっとしてOpenGL ESと共存できないとか?それとも我がシェーダーのコードに問題がある?と、悩むこと数時間。はたと「Flush系の関数があるんじゃね?」と気づいてリファレンスを見たら、あった。そして、正解だった。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <vector>

#include <agile.h>

#if !defined GL_GLEXT_PROTOTYPES
    #define GL_GLEXT_PROTOTYPES
#endif

#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#define YS_GL_ES2
typedef double GLdouble;

#include <ysgl.h>

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <EGL/eglplatform.h>
#include <angle_windowsstore.h>



using namespace Windows::ApplicationModel::Core;
using namespace Windows::ApplicationModel::Activation;
using namespace Windows::UI::Core;
using namespace Windows::UI::Input;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Graphics::Display;
using namespace Windows::System;

using namespace Platform;



ref class SingleWindowAppView sealed : public Windows::ApplicationModel::Core::IFrameworkView
{
public:
    typedef SingleWindowAppView THISCLASS;

private:  // Cannot have a public member variable?
    bool isWindowClosed,isWindowVisible;
    Platform::Agile<CoreWindow> hWnd;

    static THISCLASS ^CurrentView;

    EGLDisplay mEglDisplay;
    EGLContext mEglContext;
    EGLSurface mEglSurface;

    static std::vector <unsigned char> RenderTextToBitmap(int &w,int &h,const char msg[]);

public:
    static THISCLASS ^GetCurrentView(void);

    SingleWindowAppView();

    virtual void Initialize(Windows::ApplicationModel::Core::CoreApplicationView^ applicationView);
    virtual void SetWindow(Windows::UI::Core::CoreWindow^ window);
    virtual void Load(Platform::String^ entryPoint);
    virtual void Run();
    virtual void Uninitialize();

    virtual void OnActivated(CoreApplicationView^ applicationView, IActivatedEventArgs^ args);
    virtual void OnVisibilityChanged(CoreWindow^ sender, VisibilityChangedEventArgs^ args);
    virtual void OnWindowClosed(CoreWindow^ sender, CoreWindowEventArgs^ args);

private:
    void InitializeOpenGLES2(void);
    EGLBoolean InitializeEGLDisplayDefault(void);
    EGLBoolean InitializeEGLDisplay(int level);
    std::vector <EGLint> MakeDisplayAttribute(int level) const;
    void SetUpConfigAttrib(EGLint configAttrib[],int depthBit);
    void CleanUpOpenGLES2(void);
};

/* static */ SingleWindowAppView ^SingleWindowAppView::CurrentView;

/* static */ SingleWindowAppView ^SingleWindowAppView::GetCurrentView(void)
{
    return CurrentView;
}

/* static */ std::vector <unsigned char> SingleWindowAppView::RenderTextToBitmap(int &w,int &h,const char msg[])
{
    const int bmpWid=640,bmpHei=120;

    auto device=Microsoft::Graphics::Canvas::CanvasDevice::GetSharedDevice();
    auto bitmap=ref new Microsoft::Graphics::Canvas::CanvasRenderTarget(device,(float)bmpWid,(float)bmpHei,96);

    auto ds=bitmap->CreateDrawingSession();
    ds->Clear(Windows::UI::Colors::Green);

    auto txtFmt=ref new Microsoft::Graphics::Canvas::Text::CanvasTextFormat();
    txtFmt->FontSize=bitmap->ConvertPixelsToDips(48);

    std::vector <wchar_t> wMsg;
    for(int i=0; 0!=msg[i]; ++i)
    {
        wMsg.push_back(msg[i]);
    }
    wMsg.push_back(0);

    auto wStr=ref new Platform::String(wMsg.data());
    ds->DrawText(wStr,0,0,Windows::UI::Colors::Orange,txtFmt);

    ds->Flush(); // <- Don't forget this, or you get nothing.

    // Windows bitmap starts at top-left, while OpenGL bitmap at bottom-left.
    // Bitmap needs to be flipped.
    w=bmpWid;
    h=bmpHei;

    auto rawBitmap=bitmap->GetPixelBytes();
    std::vector <unsigned char> reallyRawBitmap;
    reallyRawBitmap.resize(w*h*4);
    {
        int nb=0,x=0,y=0;
        unsigned char bgra[4];
        for(auto a : rawBitmap)
        {
            bgra[nb++]=a;
            if(4==nb)
            {
                reallyRawBitmap[((h-y-1)*w+x)*4  ]=bgra[2];
                reallyRawBitmap[((h-y-1)*w+x)*4+1]=bgra[1];
                reallyRawBitmap[((h-y-1)*w+x)*4+2]=bgra[0];
                reallyRawBitmap[((h-y-1)*w+x)*4+3]=bgra[3];
                nb=0;
                ++x;
                if(w<=x)
                {
                    x=0;
                    ++y;
                }
            }
        }
    }

    return reallyRawBitmap;
}

SingleWindowAppView::SingleWindowAppView()
{
    isWindowClosed=false;
    isWindowVisible=false;

    mEglDisplay=EGL_NO_DISPLAY;
    mEglContext=EGL_NO_CONTEXT;
    mEglSurface=EGL_NO_SURFACE;

    CurrentView=this;
}

void SingleWindowAppView::Initialize(Windows::ApplicationModel::Core::CoreApplicationView^ applicationView)
{
    applicationView->Activated+=
        ref new TypedEventHandler<CoreApplicationView^, IActivatedEventArgs^>(this,&THISCLASS::OnActivated);
}
void SingleWindowAppView::SetWindow(Windows::UI::Core::CoreWindow^ window)
{
    hWnd=window;

    window->VisibilityChanged+=
        ref new TypedEventHandler<CoreWindow^,VisibilityChangedEventArgs^>(this,&THISCLASS::OnVisibilityChanged);
    window->Closed+=
        ref new TypedEventHandler<CoreWindow^,CoreWindowEventArgs^>(this,&THISCLASS::OnWindowClosed);

    InitializeOpenGLES2(); // Example initializes OpenGL ES context in here.
}
void SingleWindowAppView::Load(Platform::String^ entryPoint)
{
    YsGLSLCreateSharedRenderer();
}
void SingleWindowAppView::Run()
{
    while(!isWindowClosed)
    {
        if (isWindowVisible)
        {
            // This doesn't seem to wait for an event.  Like a PeekMessage
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);

            glClearColor(0,0,1,0);
            glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

            EGLint w=0,h=0;
            eglQuerySurface(mEglDisplay, mEglSurface, EGL_WIDTH, &w);
            eglQuerySurface(mEglDisplay, mEglSurface, EGL_HEIGHT, &h);
            if(0<w && 0<h)
            {
                glViewport(0,0,w,h);

                int bmpW,bmpH;
                auto bmp=RenderTextToBitmap(bmpW,bmpH,"Test Message");

                // In the BitmapRenderer, it just create and render a texture.
                // I don't explain OpenGL ES itself here.
                auto renderer=YsGLSLSharedBitmapRenderer();
                YsGLSLUseBitmapRenderer(renderer);
                YsGLSLRenderRGBABitmap2D(renderer,100,100,YSGLSL_HALIGN_LEFT,YSGLSL_VALIGN_TOP,bmpW,bmpH,bmp.data());
                YsGLSLEndUseBitmapRenderer(renderer);
            }

            if (eglSwapBuffers(mEglDisplay, mEglSurface) != GL_TRUE)
            {
                YsGLSLDeleteSharedRenderer();
                CleanUpOpenGLES2();
                InitializeOpenGLES2();
                YsGLSLCreateSharedRenderer();
            }
        }
        else
        {
            // This seems to wait until an event is thrown.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }

    CleanUpOpenGLES2(); // Example deletes OpenGL context here.
}
void SingleWindowAppView::Uninitialize()
{
}

void SingleWindowAppView::OnActivated(CoreApplicationView^ applicationView, IActivatedEventArgs^ args)
{
    CoreWindow::GetForCurrentThread()->Activate();
}
void SingleWindowAppView::OnVisibilityChanged(CoreWindow^ sender, VisibilityChangedEventArgs^ args)
{
    isWindowVisible=args->Visible;
}
void SingleWindowAppView::OnWindowClosed(CoreWindow^ sender, CoreWindowEventArgs^ args)
{
    isWindowClosed=true;
}

void SingleWindowAppView::InitializeOpenGLES2(void)
{
    // Cut & Pasted from OpenGL ES2 example.
    // Modified so that it selects the maximum depth bits.
    EGLint configAttributes[]=
    {
        EGL_RED_SIZE,8,
        EGL_GREEN_SIZE,8,
        EGL_BLUE_SIZE,8,
        EGL_ALPHA_SIZE,8,
        EGL_DEPTH_SIZE,24,
        EGL_STENCIL_SIZE,8,
        EGL_NONE
    };

    const EGLint contextAttributes[]=
    {
        EGL_CONTEXT_CLIENT_VERSION,2,
        EGL_NONE
    };

    const EGLint surfaceAttributes[]=
    {
        EGL_ANGLE_SURFACE_RENDER_TO_BACK_BUFFER,EGL_TRUE,
        EGL_NONE
    };

    EGLConfig config = NULL;

    // According to the example, the program should try to initialize OpenGL ES context with
    // EGL to D3D11 Feature Level 10_0+ first.  If it fails, try EGL to D3D11 Feature Level 9_3.
    // If it fails, try EGL to D3D11 Feature Level 11_0 on WARP.
    if(InitializeEGLDisplayDefault()!=EGL_TRUE &&
       InitializeEGLDisplay(0)!=EGL_TRUE &&
       InitializeEGLDisplay(1)!=EGL_TRUE &&
       InitializeEGLDisplay(2)!=EGL_TRUE)
    {
        throw Exception::CreateException(E_FAIL, L"Failed to initialize EGL");
    }

    // The sample code given with the OpenGL ES template uses 8-bit Z-buffer, 
    // which is pretty much useless.
    // Ideallly want to have 64-bit (or as precise as coordinates, in many cases 32-bit), 
    // but here get whatever available.
    EGLint numConfigs = 0;
    for(int depthBit=32; 8<=depthBit; depthBit-=8)
    {
        SetUpConfigAttrib(configAttributes,depthBit);
        if(eglChooseConfig(mEglDisplay,configAttributes,&config,1,&numConfigs)==EGL_TRUE && 0!=numConfigs)
        {
            printf("%d Depth Bits\n",depthBit);
            break;
        }
    }
    if(0==numConfigs)
    {
        throw Exception::CreateException(E_FAIL,L"Failed to choose first EGLConfig");
    }

    PropertySet^ surfaceCreationProperties = ref new PropertySet();
    surfaceCreationProperties->Insert(ref new String(EGLNativeWindowTypeProperty), hWnd.Get());

    mEglSurface=eglCreateWindowSurface(mEglDisplay,config,reinterpret_cast<IInspectable*>(surfaceCreationProperties),surfaceAttributes);
    if(mEglSurface==EGL_NO_SURFACE)
    {
        throw Exception::CreateException(E_FAIL,L"Failed to create EGL fullscreen surface");
    }

    mEglContext=eglCreateContext(mEglDisplay,config,EGL_NO_CONTEXT,contextAttributes);
    if(mEglContext==EGL_NO_CONTEXT)
    {
        throw Exception::CreateException(E_FAIL,L"Failed to create EGL context");
    }

    if(eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)==EGL_FALSE)
    {
        throw Exception::CreateException(E_FAIL,L"Failed to make fullscreen EGLSurface current");
    }
}

EGLBoolean SingleWindowAppView::InitializeEGLDisplayDefault(void)
{
    // The sample code tells that eglGetPlatformDisplayEXT is use in place for eglGetDisplay to pass display attributes.
    // But, why not trying eglGetDisplay first?
    mEglDisplay=eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if(EGL_NO_DISPLAY!=mEglDisplay)
    {
        auto b=eglInitialize(mEglDisplay,NULL,NULL);
        printf("Looks like the default eglGetDisplay is good enough.  Isn't it?");
        return b;
    }
    return EGL_FALSE;
}

EGLBoolean SingleWindowAppView::InitializeEGLDisplay(int level)
{
    PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT=
        reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(eglGetProcAddress("eglGetPlatformDisplayEXT"));
    if (!eglGetPlatformDisplayEXT)
    {
        throw Exception::CreateException(E_FAIL, L"Failed to get function eglGetPlatformDisplayEXT");
    }

    auto attrib=MakeDisplayAttribute(level);

    mEglDisplay=eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE,EGL_DEFAULT_DISPLAY,attrib.data());
    if(mEglDisplay!=EGL_NO_DISPLAY)
    {
        return eglInitialize(mEglDisplay,NULL,NULL);
    }
    return EGL_FALSE;
}

std::vector <EGLint> SingleWindowAppView::MakeDisplayAttribute(int level) const
{
    const EGLint commonDisplayAttributePre[]=
    {
        EGL_PLATFORM_ANGLE_TYPE_ANGLE,EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
    };
    const EGLint commonDisplayAttributePost[]=
    {
        EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER,EGL_TRUE,
        EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE,EGL_TRUE,
        EGL_NONE,
    };

    std::vector <EGLint> attrib;
    for(auto v : commonDisplayAttributePre)
    {
        attrib.push_back(v);
    }
    switch(level)
    {
    case 0:
        break;
    case 1:  // FL9.  I don't know exactly what FL9 stands for, but as example says.
        attrib.push_back(EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE);
        attrib.push_back(9);
        attrib.push_back(EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE);
        attrib.push_back(3);
        break;
    case 2:  // warp.  I don't know exactly what it means by "warp", but as example says.
        attrib.push_back(EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE);
        attrib.push_back(EGL_PLATFORM_ANGLE_DEVICE_TYPE_WARP_ANGLE);
        break;
    }
    for(auto v : commonDisplayAttributePost)
    {
        attrib.push_back(v);
    }
    return attrib;
}

void SingleWindowAppView::SetUpConfigAttrib(EGLint configAttrib[],int depthBit)
{
    for(int i=0; configAttrib[i]!=EGL_NONE; i+=2)
    {
        if(EGL_DEPTH_SIZE==configAttrib[i])
        {
            configAttrib[i+1]=depthBit;
        }
    }
}

void SingleWindowAppView::CleanUpOpenGLES2(void)
{
    // Cut & Pasted from OpenGL ES2 Example.
    if(mEglDisplay!=EGL_NO_DISPLAY && mEglSurface!=EGL_NO_SURFACE)
    {
        eglDestroySurface(mEglDisplay,mEglSurface);
        mEglSurface=EGL_NO_SURFACE;
    }
    if(mEglDisplay!=EGL_NO_DISPLAY && mEglContext!=EGL_NO_CONTEXT)
    {
        eglDestroyContext(mEglDisplay,mEglContext);
        mEglContext=EGL_NO_CONTEXT;
    }
    if(mEglDisplay!=EGL_NO_DISPLAY)
    {
        eglTerminate(mEglDisplay);
        mEglDisplay=EGL_NO_DISPLAY;
    }
}

ref class SingleWindowAppSource sealed : Windows::ApplicationModel::Core::IFrameworkViewSource
{
public:
    virtual Windows::ApplicationModel::Core::IFrameworkView^ CreateView()
    {
        return ref new SingleWindowAppView();
    }
};

[Platform::MTAThread]
int main(Platform::Array<Platform::String^>^)
{
    auto singleWindowAppSource = ref new SingleWindowAppSource();
    CoreApplication::Run(singleWindowAppSource);
    return 0;
}

 

Win2DとOpenGL ESで文字を描く (大きさ調整付き)

ただ、上の方法だと、十分大きなオフスクリーンバッファを取って描画すればテキスト全体の表示はできるけど、ちょうどテキストを描画した部分だけを抜き出すことができない。調べたら同じ問題を抱えてた人はいて、次のリンクによると、CanvasDrawingLayoutを使うと良いことがわかった。

http://stackoverflow.com/questions/30696838/how-to-calculate-the-size-of-a-piece-of-text-in-win2d

CanvasDrawingLayout、非常に惜しいのは、先にオフスクリーンビットマップ (CanvasRenderingTarget)を作った後でないと使えないので、あらかじめテキストに必要なビットマップのサイズを計算して、ちょうどその大きさのビットマップを作るということはできない。十分大きなビットマップを作っておいて、その一部を切り出すという使い方をせざるを得ない。まあ、そのぐらいは許容範囲。

上の例題から、RenderTextBitmapだけ次のように変更すればいい。いったん、CanvasDrawingLayoutを作って、それをDrawTextLayout関数で描画する。テキストが占める大きさは、CanvasDrawingLayoutから取得できる。

/* static */ std::vector <unsigned char> SingleWindowAppView::RenderTextToBitmap(int &w,int &h,const char msg[])
{
    const int bmpWid=640,bmpHei=120;

    auto device=Microsoft::Graphics::Canvas::CanvasDevice::GetSharedDevice();
    auto bitmap=ref new Microsoft::Graphics::Canvas::CanvasRenderTarget(device,(float)bmpWid,(float)bmpHei,96);

    auto ds=bitmap->CreateDrawingSession();
    ds->Clear(Windows::UI::Colors::Green);

    auto txtFmt=ref new Microsoft::Graphics::Canvas::Text::CanvasTextFormat();
    txtFmt->FontSize=bitmap->ConvertPixelsToDips(48);

    std::vector <wchar_t> wMsg;
    for(int i=0; 0!=msg[i]; ++i)
    {
        wMsg.push_back(msg[i]);
    }
    wMsg.push_back(0);

    auto wStr=ref new Platform::String(wMsg.data());

    auto txtLayout=ref new Microsoft::Graphics::Canvas::Text::CanvasTextLayout(ds,wStr,txtFmt,(float)bmpWid,(float)bmpHei);
    auto rounding=Microsoft::Graphics::Canvas::CanvasDpiRounding::Floor;
    int renderX=(int)bitmap->ConvertDipsToPixels(txtLayout->LayoutBounds.X,rounding);
    int renderY=(int)bitmap->ConvertDipsToPixels(txtLayout->LayoutBounds.Y,rounding);
    int renderW=(int)bitmap->ConvertDipsToPixels(txtLayout->LayoutBounds.Width,rounding);
    int renderH=(int)bitmap->ConvertDipsToPixels(txtLayout->LayoutBounds.Height,rounding);

    ds->DrawTextLayout(txtLayout,0,0,Windows::UI::Colors::Orange);

    ds->Flush(); // <- Don't forget this, or you get nothing.

    // When you need the bitmap size of the text on the bitmap,
    // you can do it by CanvasTextLayout.
    // http://stackoverflow.com/questions/30696838/how-to-calculate-the-size-of-a-piece-of-text-in-win2d

    // Windows bitmap starts at top-left, while OpenGL bitmap at bottom-left.
    // Bitmap needs to be flipped.
    w=renderW;
    h=renderH;

    auto rawBitmap=bitmap->GetPixelBytes();
    std::vector <unsigned char> reallyRawBitmap;
    reallyRawBitmap.resize(renderW*renderH*4);
    {
        int x=0,y=0;
        for(int y=0; y<renderH; ++y)
        {
            for(int x=0; x<renderW; ++x)
            {
                if(x<bmpWid && y<bmpHei)
                {
                    // Windows -> BGRA   OpenGL -> RGBA  Need conversion.
                    reallyRawBitmap[((h-y-1)*w+x)*4  ]=rawBitmap[(y*bmpWid+x)*4+2];
                    reallyRawBitmap[((h-y-1)*w+x)*4+1]=rawBitmap[(y*bmpWid+x)*4+1];
                    reallyRawBitmap[((h-y-1)*w+x)*4+2]=rawBitmap[(y*bmpWid+x)*4];
                    reallyRawBitmap[((h-y-1)*w+x)*4+3]=rawBitmap[(y*bmpWid+x)*4+3];
                }
                else
                {
                    reallyRawBitmap[((h-y-1)*w+x)*4  ]=0;
                    reallyRawBitmap[((h-y-1)*w+x)*4+1]=0;
                    reallyRawBitmap[((h-y-1)*w+x)*4+2]=0;
                    reallyRawBitmap[((h-y-1)*w+x)*4+3]=0;
                }
            }
        }
    }

    return reallyRawBitmap;
}

 

例題ファイルのコンパイル

例題のコンパイルには、最低CMake 3.8が必要。例題ファイルを、例えば、ユーザーディレクトリ直下に展開したとすると、次のようなコマンドで、CMakeをかけて、Visual Studioでプロジェクトを開くことができる。

C:\users\soji\samples\05_win2d_renderText>mkdir build

C:\users\soji\samples\05_win2d_renderText>cd build

C:\users\soji\samples\05_win2d_renderText\build>CMake .. -G "Visual Studio 14 2015" -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0

C:\users\soji\samples\05_win2d_renderText\build>start Project.sln

Comments are welcome.  Send E-Mail to: 

Back to http://www.ysflight.com