C++使用Uniscribe进行文字自动换行的计算和渲染

Uniscribe是Windows 2000以来就存在于WinAPI中的一个库。这个库能够提供给我们关于字符串渲染的很多信息,譬如说哪里可以换行啦,渲染的时候字符的顺序应该是什么样子啦,还有每一个字符的大小什么的。关于Uniscribe的资料可以在http://msdn.microsoft.com/en-us/library/windows/desktop/dd374091(v=vs.85).aspx看到。

在使用Uniscribe之前,我们先看看利用Uniscribe我们可以做到什么样的效果:

image

image

通过Uniscribe,我们可以获得把各种不同大小的字符串混合在一起渲染的时候所需要的所有数据,甚至可以再漂亮的地方换行,譬如说这里:

image

当然,渲染的部分不包含在Uniscribe里面,只不过Uniscribe告诉我们的信息可以让我们直接计算出渲染每一小段字符串的位置。当然,这也就足够了。下面我来介绍一下Uniscribe的几个函数的作用。

首先,我们需要注意的是,Uniscribe一次只处理一行字符串。我们固然可以把多行字符串一次性丢给Uniscribe进行计算,但是得到的结果处理起来要困难得多。所以我们一次只给Uniscribe一行的字符串。现在我们需要渲染一个带有多种格式的一行字符串。首先我们需要知道这些字符串可以被分为多少段。这在那些从右到左阅读的文字(譬如说阿拉伯文)特别重要,而且这也是一个特别复杂的话题,在这里我就不讲了,我们假设我们只处理从左到右的字符串。

于是我们第一个遇到的函数就是ScriptItemize
HRESULT ScriptItemize(
  _In_      const WCHAR *pwcInChars,
  _In_      int cInChars,
  _In_      int cMaxItems,
  _In_opt_  const SCRIPT_CONTROL *psControl,
  _In_opt_  const SCRIPT_STATE *psState,
  _Out_     SCRIPT_ITEM *pItems,
  _Out_     int *pcItems
);

由于我们不处理从右到左的字符串渲染,也不处理把数字变成乱七八糟格式的效果(譬如在某些邪恶的帝国主义国家12345被表达成12,345),因此这个函数的psControl和psState参数我们都可以给NULL。这个时候我们需要首先为SCRIPT_ITEM数组分配空间。由于一个字符串的item最多就是字符数量那么多个,所以我们要先创建一个cInChars+1那么长的SCRIPT_ITEM数组。在调用了这个函数之后,*pcItems+1的结果就是pItems里面的有效长度了。为什么pItems的长度总是要+1呢?因为SCRIPT_ITEM里面有一个很有用的成员叫做iCharPos,这个成员告诉我们这个item是从字符串的什么地方开始的。那长度呢?自然是用下一个SCRIPT_ITEM的cCharPos去剪了。那么最后一个item怎么办呢?所以ScriptItemize给了我们额外的一个结尾item,让我们总是可以方便的这么减……特别的蛋疼……

好了,现在我们把一行字符串分成了各个item。现在第一个问题就来了,一行字符串里面可能有各种不同的字体的样式,接下来怎么办呢?我们要同时用item的边界和样式的边界来切割这个字符串,让每一个字符串的片段都完全被某个item包含,并且片段的所有字符都有一样的样式。这听起来好像很复杂,我来举个例子:

譬如我们有一个字符串长成下面这个样子:
This parameter (foo) is optional

然后ScriptItemize告诉我们这个字符串一共分为3个片段(这个划分当然是我胡扯的,我只是举个例子):
This parameter
(foo)
is optional

所以,字体的样式和ScriptItemize的结果就把这个字符串分成了下面的五段:
This
parameter
(foo)
is
optional

是不是听起来很直观呢?但是代码写起来还是比较麻烦的,不过其实说麻烦也不麻烦,只需要大约十行左右就可以搞定了。在MSDN里面,这五段的“段”叫做“run”或者是“range”。

现在,我们拿起一个run,送进一个叫做ScriptShape的函数里面:
HRESULT ScriptShape(
  _In_     HDC hdc,
  _Inout_  SCRIPT_CACHE *psc,
  _In_     const WCHAR *pwcChars,
  _In_     int cChars,
  _In_     int cMaxGlyphs,
  _Inout_  SCRIPT_ANALYSIS *psa,
  _Out_    WORD *pwOutGlyphs,
  _Out_    WORD *pwLogClust,
  _Out_    SCRIPT_VISATTR *psva,
  _Out_    int *pcGlyphs
);

这个函数可以告诉我们,这一堆wchar_t可以被如何分割成glyph。这里我们要注意的是,glyph的数量和wchar_t的数量并不相同。所以在调用这个函数的时候,我们要先猜一个长度来分配空间。MSDN告诉我们,我们可以先让cMaxGlyphs = cChars*1.5 + 16。

在上面的参数里,SCRIPT_ANALYSIS其实就是SCRIPT_ITEM::a。由于一个run肯定是完整的属于一个item的,因此SCRIPT_ITEM就可以直接从上一个函数的结果获得了。然后这个函数告诉我们三个信息:
1、pwOutGlyphs:这个字符串一共有多少glyph组成。
2、psva:每一个glyph的属性是什么。
3、pwLogClust:wchar_t(术语叫unicode code point)是如何跟glyph对应起来的。

在这里解释一下glyph是什么意思。glyph其实就是字体里面的一个“图”。一个看起来像一个字符的东西,有可能由多个glyph组成,譬如说“á”,其实就占用了两个wchar_t,同时这两个wchar_t具有两个glyph(a和上面的小点)。而且这两个wchar_t在渲染的时候必须被渲染在一起,因此他们至少应该属于同一个range,鼠标在文本框选中的时候,这两个wchar_t必须作为一个整体(后面这些信息可以由ScriptBreak函数给出)。当然还有1个wchar_t对多个glyph的情况,但是我现在一下子找不到。

不仅如此,还有两个wchar_t对一个glyph的情况,譬如说这些字“㦲 ”。虽然wchar_t的范围是0到65536,但这并不代表utf-16只有6万多个字符(实际上是60多万),所以wchar_t其实也是变长的。但是utf-16的编码设计的很好,当我们拿到一个wchar_t的时候,我们通过阅读他们的数字就可以知道这个wchar_t是只有一个code point的、还是那些两个code point的字的第一个或者是第二个,跟我们以前遇到的MBCS(char/ANSI)完全不同。

因此wchar_t和glyph的对应关系很复杂,可能是一对多、多对一、一对一或者多对多。所以pwLogClust这个数组就特别的重要。MSDN里面有一个例子:

譬如说我们的一个7个wchar_t的字符串被分成4组glyph,对应关系如下:
字符:| c1u1 | c2u1 | c3u1 c3u2 c3u3 | c4u1 c4u2 |
图案:| c1g1 | c2g1 c2g2 c2g3 | c3g1 | c4g1 c4g2 c4g3 |

上面的意思是,第二个字符c2u2被渲染成了3个glyph:c2g1、c2g2和c2g3,而c3u1、c3u2和c3u3三个字符责备合并成了一个glyph:c3g1。这种情况下,pwLogClust[cChars]的内容就是下面这个样子的:
| 0 | 1 | 4 4 4 | 5 5 |

连续的数字相同的几个clust说明这些wchar_t是被归到一起的,而且这一组wchar_t的第一个glyph的的序号就是pwLogClust的内容了。那么这一组wchar_t究竟有多少个glyph呢?当然就要看下一组wchar_t的第一个glyph在哪了。

为什么我们需要这些信息呢?因为字符串的长度是按照glyph的长度来计算的!而且接下来我们要介绍的函数ScriptPlace会真的给我们每一个glyph的长度。因此我们在计算换行的时候,我们只能在每一组glyph并且ScriptBreak告诉我们可以换行的那个地方换行,所以当我们拿出一段完整的不会被换行的一个run的子集的时候,我们要在渲染的时候计算长度,就要特别小心glyph和wchar_t的对应关系。因为我们渲染的是一串wchar_t,但是我们的长度是按照glyph计算的,这个对应关系要是乱掉了,要么计算出错,要么渲染的字符选错,总之是很麻烦的。那么ScriptPlace究竟长什么样子呢:
HRESULT ScriptPlace(
  _In_     HDC hdc,
  _Inout_  SCRIPT_CACHE *psc,
  _In_     const WORD *pwGlyphs,
  _In_     int cGlyphs,
  _In_     const SCRIPT_VISATTR *psva,
  _Inout_  SCRIPT_ANALYSIS *psa,
  _Out_    int *piAdvance,
  _Out_    GOFFSET *pGoffset,
  _Out_    ABC *pABC
);

这就是那个传说中的帮我们计算glyph大小的函数了。其中pwGlyphs就是我们刚刚从ScriptShape函数拿到的pwOutGlyphs,而psa还是那个psa,psva也还是那个psva。接下来的piAdvance数组告诉我们每一个glyph的长度,pGoffset这个是每一个glyph的偏移量(还记得“á”上面的那个小点吗),pABC是整一个run的长度。至于ABC的三个长度我们并不用管,因为我们需要的是pABC里面三个长度的和。而且这个和跟piAdvance的所有数字加起来一样。

现在我们拿到了所有glyph的尺寸信息,和他们的分组情况,最后就是知道字符串的一些属性了,譬如说在哪里可以换行。为什么要知道这些呢?譬如说我们有一个字符串叫做
c:\ThisIsAFolder\ThisIsAFile.txt

然后我们渲染字符串的位置可以容纳下“c:\ThisIsAFolder\”,却不能容纳完整的“c:\ThisIsAFolder\ThisIsAFile”。这个时候,ScriptBreak函数就可以告诉我们,一个优美的换行可以在斜杠“\”的后面产生。让我们来看看这个ScriptBreak函数的真面目:
HRESULT ScriptBreak(
  _In_   const WCHAR *pwcChars,
  _In_   int cChars,
  _In_   const SCRIPT_ANALYSIS *psa,
  _Out_  SCRIPT_LOGATTR *psla
);

这个函数告诉我们每一个wchar_t对应的SCRIPT_LOGATTR。这个结构我们暂时只关心下面几个成员:
1、fSoftBreak:可以被换行的位置。譬如说上面那个美妙的换行在“\”处,就是因为接下来的ThisIsAFile的第一个字符“T”的fSoftBreak是TRUE。
2、fCharStop和fWordStop:告诉我们每一个wchar_t是不是char或者word的第一个code point(参考那些一个字有两个wchar_t那么长的㦲 )。

现在我们距离大功告成已经很近了。我们在渲染的时候,就一个run一个run的渲染。当我们发现一行剩余的空间不够容纳一个完整的run的时候,我们就可以用ScriptBreak告诉我们的信息,把这个run看成若干个可以被切开的段,然后用ScriptPlace告诉我们的piAdvance算出每一个切开的小段落的长度,然后尽可能多的完整渲染这些段。

上面这段话虽然很简单,但是实际上需要注意的事情特别多,譬如说那个复杂的wchar_t和glyph的关系。我们通过piAdvance计算出可以一次性渲染的glyph有多少个,再把通过ScriptShape告诉我们的pwLogClust把这些glyph换算成对应wchar_t的范围。最后再把他们送进TextOut函数里,如果你用的是GDI的话。每次渲染完一些glyph,x坐标就要偏移他们的piAdvances的和。

如果把上面这些事情全部做完的话,我们就已经完整的渲染出一行带有复杂结构的文字了。

=========================================================

最后我贴上这个程序的代码。这个程序使用GacUI编写,中间的部分使用GDI进行渲染。由于这只是个临时代码,会从codeplex上删掉,所以把代码留在这里,给有需要的人阅读。

代码里面用到的这个叫document.txt的文件,可以在GacUI的Codeplex页面上下载代码后,在(\Libraries\GacUI\GacUISrc\GacUISrcCodepackedTest\Resources\document.txt)找到

#include <GacUI.h>
#include <usp10.h>

#pragma comment(lib, "usp10.lib")

using namespace vl::collections;
using namespace vl::stream;
using namespace vl::regex;
using namespace vl::presentation::windows;

int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int CmdShow)
{
    return SetupWindowsGDIRenderer();
}

/***********************************************************************
Uniscribe
***********************************************************************/

bool operator==(const SCRIPT_ITEM&, const SCRIPT_ITEM&){return false;}
bool operator!=(const SCRIPT_ITEM&, const SCRIPT_ITEM&){return false;}

bool operator==(const SCRIPT_VISATTR&, const SCRIPT_VISATTR&){return false;}
bool operator!=(const SCRIPT_VISATTR&, const SCRIPT_VISATTR&){return false;}

bool operator==(const GOFFSET&, const GOFFSET&){return false;}
bool operator!=(const GOFFSET&, const GOFFSET&){return false;}

bool operator==(const SCRIPT_LOGATTR&, const SCRIPT_LOGATTR&){return false;}
bool operator!=(const SCRIPT_LOGATTR&, const SCRIPT_LOGATTR&){return false;}

namespace test
{

/***********************************************************************
DocumentFragment
***********************************************************************/

    class DocumentFragment : public Object
    {
    public:
        bool                paragraph;
        WString                font;
        bool                bold;
        Color                color;
        int                    size;
        WString                text;
        Ptr<WinFont>        fontObject;

        DocumentFragment()
            :paragraph(false)
            ,bold(false)
            ,size(0)
        {
        }

        DocumentFragment(Ptr<DocumentFragment> prototype, const WString& _text)
            :paragraph(prototype->paragraph)
            ,font(prototype->font)
            ,bold(prototype->bold)
            ,color(prototype->color)
            ,size(prototype->size)
            ,fontObject(prototype->fontObject)
            ,text(_text)
        {
        }

        WString GetFingerPrint()
        {
            return font+L":"+(bold?L"B":L"N")+L":"+itow(size);
        }
    };

    int ConvertHex(wchar_t c)
    {
        if(L'a'<=c && c<=L'f') return c-L'a'+10;
        if(L'A'<=c && c<=L'F') return c-L'A'+10;
        if(L'0'<=c && c<=L'9') return c-L'0';
        return 0;
    }

    Color ConvertColor(const WString& colorString)
    {
        return Color(
            ConvertHex(colorString[1])*16+ConvertHex(colorString[2]),
            ConvertHex(colorString[3])*16+ConvertHex(colorString[4]),
            ConvertHex(colorString[5])*16+ConvertHex(colorString[6])
            );
    }

    void BuildDocumentFragments(const WString& fileName, List<Ptr<DocumentFragment>>& fragments)
    {
        fragments.Clear();
        WString rawDocument;
        {
            FileStream fileStream(fileName, FileStream::ReadOnly);
            Utf8Decoder decoder;
            DecoderStream decoderStream(fileStream, decoder);
            StreamReader reader(decoderStream);
            rawDocument=reader.ReadToEnd();
        }

        Regex regex(L"<(<tag>s)>(<font>[^:]+):(<bold>[^:]+):(<color>[^:]+):(<size>[^:]+):(<text>/.*?)<//s>|<(<tag>p)//>");
        RegexMatch::List matches;
        regex.Search(rawDocument, matches);
       
        for(int i=0;i<matches.Count();i++)
        {
            Ptr<RegexMatch> match=matches[i];
            Ptr<DocumentFragment> fragment=new DocumentFragment;
            fragments.Add(fragment);
            if(match->Groups()[L"tag"][0].Value()==L"p")
            {
                fragment->paragraph=true;
            }
            else
            {
                WString font=match->Groups()[L"tag"][0].Value();
                WString bold=match->Groups()[L"bold"][0].Value();
                WString color=match->Groups()[L"color"][0].Value();
                WString size=match->Groups()[L"size"][0].Value();
                WString text=match->Groups()[L"text"][0].Value();

                fragment->font=font;
                fragment->bold=bold==L"true";
                fragment->size=wtoi(size);
                fragment->color=ConvertColor(color);
                fragment->text=text;
            }
        }
    }

/***********************************************************************
ScriptFragment
***********************************************************************/
   
    struct GlyphData
    {
        Array<WORD>                    glyphs;
        Array<SCRIPT_VISATTR>        glyphVisattrs;
        Array<int>                    glyphAdvances;
        Array<GOFFSET>                glyphOffsets;
        Array<WORD>                    charCluster;
        ABC                            runAbc;

        GlyphData()
        {
            memset(&runAbc, 0, sizeof(runAbc));
        }

        void ClearUniscribeData(int glyphCount, int length)
        {
            glyphs.Resize(glyphCount);
            glyphVisattrs.Resize(glyphCount);
            glyphAdvances.Resize(glyphCount);
            glyphOffsets.Resize(glyphCount);
            charCluster.Resize(length);
            memset(&runAbc, 0, sizeof(runAbc));
        }
           
        bool BuildUniscribeData(WinDC* dc, DocumentFragment* documentFragment, SCRIPT_ITEM* scriptItem, SCRIPT_CACHE& scriptCache, const wchar_t* runText, int length)
        {
            int glyphCount=glyphs.Count();
            bool resizeGlyphData=false;
            if(glyphCount==0)
            {
                glyphCount=(int)(1.5*length+16);
                resizeGlyphData=true;
            }
            {
                // generate shape information
                WinDC* dcParameter=0;
                if(resizeGlyphData)
                {
                    glyphs.Resize(glyphCount);
                    glyphVisattrs.Resize(glyphCount);
                    charCluster.Resize(length);
                }

                while(true)
                {
                    int availableGlyphCount=0;
                    HRESULT hr=ScriptShape(
                        (dcParameter?dcParameter->GetHandle():NULL),
                        &scriptCache,
                        runText,
                        length,
                        glyphCount,
                        &scriptItem->a,
                        &glyphs[0],
                        &charCluster[0],
                        &glyphVisattrs[0],
                        &availableGlyphCount
                        );
                    if(hr==0)
                    {
                        glyphCount=availableGlyphCount;
                        break;
                    }
                    else if(hr==E_PENDING)
                    {
                        dcParameter=dc;
                    }
                    else if(hr==E_OUTOFMEMORY)
                    {
                        if(resizeGlyphData)
                        {
                            glyphCount+=length;
                        }
                        else
                        {
                            goto BUILD_UNISCRIBE_DATA_FAILED;
                        }
                    }
                    else
                    {
                        goto BUILD_UNISCRIBE_DATA_FAILED;
                    }
                }
                if(resizeGlyphData)
                {
                    glyphs.Resize(glyphCount);
                    glyphVisattrs.Resize(glyphCount);
                }
            }
            {
                // generate place information
                WinDC* dcParameter=0;
                if(resizeGlyphData)
                {
                    glyphAdvances.Resize(glyphCount);
                    glyphOffsets.Resize(glyphCount);
                }
                while(true)
                {
                    HRESULT hr=ScriptPlace(
                        (dcParameter?dcParameter->GetHandle():NULL),
                        &scriptCache,
                        &glyphs[0],
                        glyphCount,
                        &glyphVisattrs[0],
                        &scriptItem->a,
                        &glyphAdvances[0],
                        &glyphOffsets[0],
                        &runAbc
                        );
                    if(hr==0)
                    {
                        break;
                    }
                    else if(hr==E_PENDING)
                    {
                        dcParameter=dc;
                    }
                    else
                    {
                        goto BUILD_UNISCRIBE_DATA_FAILED;
                    }
                }
            }

            return true;
BUILD_UNISCRIBE_DATA_FAILED:
            return false;
        }
    };

    class ScriptRun : public Object
    {
    public:

        DocumentFragment*                documentFragment;
        SCRIPT_ITEM*                    scriptItem;
        int                                start;
        int                                length;
        const wchar_t*                    runText;

        SCRIPT_CACHE                    scriptCache;
        Array<SCRIPT_LOGATTR>            charLogattrs;
        int                                advance;
        GlyphData                        wholeGlyph;
        GlyphData                        tempGlyph;

        ScriptRun()
            :documentFragment(0)
            ,scriptItem(0)
            ,start(0)
            ,length(0)
            ,scriptCache(0)
            ,advance(0)
        {
        }

        ~ScriptRun()
        {
            ClearUniscribeData();
        }

        void ClearUniscribeData()
        {
            if(scriptCache)
            {
                ScriptFreeCache(&scriptCache);
                scriptCache=0;
            }
            charLogattrs.Resize(0);
            advance=0;
            wholeGlyph.ClearUniscribeData(0, 0);
            tempGlyph.ClearUniscribeData(0, 0);
        }

        bool BuildUniscribeData(WinDC* dc)
        {
            ClearUniscribeData();
            {
                // generate break information
                charLogattrs.Resize(length);

                HRESULT hr=ScriptBreak(
                    runText,
                    length,
                    &scriptItem->a,
                    &charLogattrs[0]
                    );
                if(hr!=0)
                {
                    goto BUILD_UNISCRIBE_DATA_FAILED;
                }
            }

            dc->SetFont(documentFragment->fontObject);
            if(!wholeGlyph.BuildUniscribeData(dc, documentFragment, scriptItem, scriptCache, runText, length))
            {
                goto BUILD_UNISCRIBE_DATA_FAILED;
            }
            tempGlyph.ClearUniscribeData(wholeGlyph.glyphs.Count(), length);
            advance=wholeGlyph.runAbc.abcA+wholeGlyph.runAbc.abcB+wholeGlyph.runAbc.abcC;

            return true;
BUILD_UNISCRIBE_DATA_FAILED:
            ClearUniscribeData();
            return false;
        }

        int SumWidth(int charStart, int charLength)
        {
            int cluster=wholeGlyph.charCluster[charStart];
            int nextCluster
                =charStart+charLength==length
                ?wholeGlyph.glyphs.Count()
                :wholeGlyph.charCluster[charStart+charLength];
            int width=0;
            for(int i=cluster;i<nextCluster;i++)
            {
                width+=wholeGlyph.glyphAdvances[i];
            }
            return width;
        }

        void SearchForLineBreak(int tempStart, int maxWidth, bool firstRun, int& charLength, int& charAdvances)
        {
            int width=0;
            charLength=0;
            charAdvances=0;
            for(int i=tempStart;i<=length;)
            {
                if(i==length || charLogattrs[i].fSoftBreak==TRUE)
                {
                    if(width<=maxWidth || (firstRun && charLength==0))
                    {
                        charLength=i-tempStart;
                        charAdvances=width;
                    }
                    else
                    {
                        return;
                    }
                }
                if(i==length) break;

                int cluster=wholeGlyph.charCluster[i];
                int clusterLength=1;
                while(i+clusterLength<length)
                {
                    if(wholeGlyph.charCluster[i+clusterLength]==cluster)
                    {
                        clusterLength++;
                    }
                    else
                    {
                        break;
                    }
                }

                int nextCluster
                    =i+clusterLength==length
                    ?wholeGlyph.glyphs.Count()
                    :wholeGlyph.charCluster[i+clusterLength];
                for(int j=cluster;j<nextCluster;j++)
                {
                    width+=wholeGlyph.glyphAdvances[j];
                }
                i+=clusterLength;
            }
        }

        bool BuildUniscribeDataTemp(WinDC* dc, int tempStart, int tempLength)
        {
            return tempGlyph.BuildUniscribeData(dc, documentFragment, scriptItem, scriptCache, runText+tempStart, tempLength);
        }
    };

    class ScriptLine : public Object
    {
    public:
        List<Ptr<DocumentFragment>>        documentFragments;
        WString                            lineText;

        Array<SCRIPT_ITEM>                scriptItems;
        List<Ptr<ScriptRun>>            scriptRuns;

        void CLearUniscribeData()
        {
            scriptItems.Resize(0);
            scriptRuns.Clear();
        }

        bool BuildUniscribeData(WinDC* dc)
        {
            lineText=L"";
            CLearUniscribeData();

            FOREACH(Ptr<DocumentFragment>, fragment, documentFragments.Wrap())
            {
                lineText+=fragment->text;
            }

            if(lineText!=L"")
            {
                {
                    // itemize a line
                    scriptItems.Resize(lineText.Length()+2);
                    int scriptItemCount=0;
                    HRESULT hr=ScriptItemize(
                        lineText.Buffer(),
                        lineText.Length(),
                        scriptItems.Count()-1,
                        NULL,
                        NULL,
                        &scriptItems[0],
                        &scriptItemCount
                        );
                    if(hr!=0)
                    {
                        goto BUILD_UNISCRIBE_DATA_FAILED;
                    }
                    scriptItems.Resize(scriptItemCount+1);
                }
                {
                    // use item and document fragment information to produce runs
                    // one item is constructed by one or more runs
                    // characters in each run contains the same style
                    int fragmentIndex=0;
                    int fragmentStart=0;
                    for(int i=0;i<scriptItems.Count()-1;i++)
                    {
                        SCRIPT_ITEM* scriptItem=&scriptItems[i];
                        int start=scriptItem[0].iCharPos;
                        int length=scriptItem[1].iCharPos-scriptItem[0].iCharPos;
                        int currentStart=start;

                        while(currentStart<start+length)
                        {
                            DocumentFragment* fragment=0;
                            int itemRemainLength=length-(currentStart-start);
                            int fragmentRemainLength=0;
                            while(true)
                            {
                                fragment=documentFragments[fragmentIndex].Obj();
                                fragmentRemainLength=fragment->text.Length()-(currentStart-fragmentStart);
                                if(fragmentRemainLength<=0)
                                {
                                    fragmentStart+=fragment->text.Length();
                                    fragmentIndex++;
                                }
                                else
                                {
                                    break;
                                }
                            }
                            int shortLength=itemRemainLength<fragmentRemainLength?itemRemainLength:fragmentRemainLength;

                            Ptr<ScriptRun> run=new ScriptRun;
                            run->documentFragment=fragment;
                            run->scriptItem=scriptItem;
                            run->start=currentStart;
                            run->length=shortLength;
                            run->runText=lineText.Buffer()+currentStart;
                            scriptRuns.Add(run);
                            currentStart+=shortLength;
                        }
                    }

                    // for each run, generate shape information
                    FOREACH(Ptr<ScriptRun>, run, scriptRuns.Wrap())
                    {
                        if(!run->BuildUniscribeData(dc))
                        {
                            goto BUILD_UNISCRIBE_DATA_FAILED;
                        }
                    }
                }
            }
            return true;
BUILD_UNISCRIBE_DATA_FAILED:
            CLearUniscribeData();
            return false;
        }
    };

    class ScriptParagraph : public Object
    {
    public:
        List<Ptr<ScriptLine>>            lines;
    };

    class ScriptDocument : public Object
    {
    public:
        List<Ptr<ScriptParagraph>>        paragraphs;
    };

    Ptr<ScriptDocument> BuildScriptParagraphs(List<Ptr<DocumentFragment>>& fragments)
    {
        Ptr<ScriptDocument> document=new ScriptDocument;
        document->paragraphs.Clear();
        Regex regex(L"\r\n");
        Ptr<ScriptParagraph> currentParagraph;
        Ptr<ScriptLine> currentLine;
        Dictionary<WString, Ptr<WinFont>> fonts;

        FOREACH(Ptr<DocumentFragment>, fragment, fragments.Wrap())
        {
            WString fragmentFingerPrint=fragment->GetFingerPrint();
            int index=fonts.Keys().IndexOf(fragmentFingerPrint);
            if(index==-1)
            {
                fragment->fontObject=new WinFont(fragment->font, fragment->size, 0, 0, 0, (fragment->bold?FW_BOLD:FW_NORMAL), false, false, false, true);
                fonts.Add(fragmentFingerPrint, fragment->fontObject);
            }
            else
            {
                fragment->fontObject=fonts.Values()[index];
            }

            if(!currentParagraph)
            {
                currentParagraph=new ScriptParagraph;
                document->paragraphs.Add(currentParagraph);
            }
           
            if(fragment->paragraph)
            {
                currentParagraph=0;
                currentLine=0;
            }
            else
            {
                RegexMatch::List matches;
                regex.Split(fragment->text, true, matches);
                for(int i=0;i<matches.Count();i++)
                {
                    Ptr<RegexMatch> match=matches[i];
                    if(i>0)
                    {
                        currentLine=0;
                    }
                    if(!currentLine)
                    {
                        currentLine=new ScriptLine;
                        currentParagraph->lines.Add(currentLine);
                    }
                    currentLine->documentFragments.Add(new DocumentFragment(fragment, match->Result().Value()));
                }
            }
        }

        HDC hdc=CreateCompatibleDC(NULL);
        WinProxyDC dc;
        dc.Initialize(hdc);
        FOREACH(Ptr<ScriptParagraph>, paragraph, document->paragraphs.Wrap())
        {
            FOREACH(Ptr<ScriptLine>, line, paragraph->lines.Wrap())
            {
                line->BuildUniscribeData(&dc);
            }
        }
        DeleteDC(hdc);

        return document;
    }

/***********************************************************************
TestWindow
***********************************************************************/

    class TestWindow : public GuiWindow
    {
    protected:
        Ptr<ScriptDocument>                document;
        Ptr<WinFont>                    messageFont;

        void element_Rendering(GuiGraphicsComposition* composition, GuiGDIElementEventArgs& arguments)
        {
            WinDC* dc=arguments.dc;
            Rect bounds=arguments.bounds;
            if(document)
            {
                int x=bounds.Left()+10;
                int y=bounds.Top()+10;
                int w=bounds.Width()-20;
                int h=bounds.Height()-10;
                int cx=0;
                int cy=0;
                const int lineDistance=5;
                const int paragraphDistance=10;

                FOREACH(Ptr<ScriptParagraph>, paragraph, document->paragraphs.Wrap())
                {
                    if(cy>=h) break;
                    FOREACH(Ptr<ScriptLine>, line, paragraph->lines.Wrap())
                    {
                        if(line->scriptRuns.Count()==0)
                        {
                            // if this line doesn't contains any run, skip and render a blank line
                            cy+=line->documentFragments[0]->size+lineDistance;
                        }
                        else
                        {
                            // render this line into linces with auto line wrapping
                            int startRun=0;
                            int startRunOffset=0;
                            int lastRun=0;
                            int lastRunOffset=0;
                            int currentWidth=0;

                            while(startRun<line->scriptRuns.Count())
                            {
                                int currentWidth=0;
                                bool firstRun=true;
                                // search for a range to fit in the given width
                                for(int i=startRun;i<line->scriptRuns.Count();i++)
                                {
                                    int charLength=0;
                                    int charAdvances=0;
                                    ScriptRun* run=line->scriptRuns[i].Obj();
                                    run->SearchForLineBreak(lastRunOffset, w-currentWidth, firstRun, charLength, charAdvances);
                                    firstRun=false;

                                    if(charLength==run->length-lastRunOffset)
                                    {
                                        lastRun=i+1;
                                        lastRunOffset=0;
                                        currentWidth+=charAdvances;
                                    }
                                    else
                                    {
                                        lastRun=i;
                                        lastRunOffset=lastRunOffset+charLength;
                                        break;
                                    }
                                }

                                // if the range is empty, than this should be the end of line, ignore it
                                if(startRun<lastRun || (startRun==lastRun && startRunOffset<lastRunOffset))
                                {
                                    // calculate the max line height in this range;
                                    int maxHeight=0;
                                    for(int i=startRun;i<=lastRun && i<line->scriptRuns.Count();i++)
                                    {
                                        int size=line->scriptRuns[i]->documentFragment->size;
                                        if(maxHeight<size)
                                        {
                                            maxHeight=size;
                                        }
                                    }

                                    // render all runs inside this range
                                    for(int i=startRun;i<=lastRun && i<line->scriptRuns.Count();i++)
                                    {
                                        ScriptRun* run=line->scriptRuns[i].Obj();
                                        int start=i==startRun?startRunOffset:0;
                                        int end=i==lastRun?lastRunOffset:run->length;
                                        int length=end-start;
                                           
                                        Color color=run->documentFragment->color;
                                        dc->SetFont(run->documentFragment->fontObject);
                                        dc->SetTextColor(RGB(color.r, color.g, color.b));
                                        dc->DrawBuffer(x+cx, y+cy+(maxHeight-run->documentFragment->size), run->runText+start, length);

                                        cx+=run->SumWidth(start, length);
                                    }

                                    cx=0;
                                    cy+=maxHeight+lineDistance;
                                }

                                startRun=lastRun;
                                startRunOffset=lastRunOffset;
                            }
                        }
                    }
                    cy+=paragraphDistance;
                }
            }
            else
            {
                dc->SetFont(messageFont);
                WString message=L"Initializing uniscribe data...";
                SIZE size=dc->MeasureString(message);
                int x=bounds.Left()+(bounds.Width()-size.cx)/2;
                int y=bounds.Top()+(bounds.Height()-size.cy)/2;
                dc->DrawString(x, y, message);
            }
        }
    public:
        TestWindow()
            :GuiWindow(GetCurrentTheme()->CreateWindowStyle())
        {
            SetText(L"GacUISrc Test Application");
            SetClientSize(Size(640, 480));
            GetBoundsComposition()->SetPreferredMinSize(Size(320, 240));
            MoveToScreenCenter();
            {
                GuiGDIElement* element=GuiGDIElement::Create();
                element->Rendering.AttachMethod(this, &TestWindow::element_Rendering);
           
                GuiBoundsComposition* composition=new GuiBoundsComposition;
                composition->SetOwnedElement(element);
                composition->SetAlignmentToParent(Margin(0, 0, 0, 0));
                GetContainerComposition()->AddChild(composition);

                messageFont=new WinFont(L"Segoe UI", 56, 0, 0, 0,FW_NORMAL, false, false, false, true);
            }
            GetApplication()->InvokeAsync([=]()
            {
                List<Ptr<DocumentFragment>> fragments;
                BuildDocumentFragments(L"..\\GacUISrcCodepackedTest\\Resources\\document.txt", fragments);
                Ptr<ScriptDocument> scriptDocument=BuildScriptParagraphs(fragments);
                GetApplication()->InvokeInMainThreadAndWait([=]()
                {
                    document=scriptDocument;
                });
            });
        }
    };
}
using namespace test;

void GuiMain()
{
    TestWindow window;
    GetApplication()->Run(&window);
}

posted on 2012-11-06 22:40  陈梓瀚(vczh)  阅读(4073)  评论(1编辑  收藏  举报