【LibUIDK界面库系列文章】使用RichEdit制作QQ聊天记录控件



作者:刘树伟
前段时间使用LibUIDK界面库为客户定制一个IM软件界面,需要把聊天记录以气泡形式展示出来。目前,国内IM软件显示聊天气泡主流的分为RichEdit和网页两派。不论从稳定性、兼容性、性能、体积、用户体验方面,RichEdit都完胜网页。不过,RichEdit开发气泡效果,难度非常大,非一般公司和个人可以解决,现在国内编程都走快餐经济,很少有人能静下心来,花几个月时间研究这个。RichEdit显示气泡,原理极其简单,但具体开发的时候,坑实在是太多。所以,我写下本文,让大家有个参考,少走弯路。


RichEdit分有句柄和无句柄两种模式,它们原理一样,本文以带句柄的RichEdit讲解。并且为了兼容性,我们也不假定RichEdit的版本,也就是说,本文的方法,适配从xp到win10各版本的RichEdit,这样,用户在使用本文方法制作出的聊天控件发布的时候,不需要附带RichEdit的DLL。



零、题外话:


制作IM软件的聊天记录控件,目前有两个阵营:一种是使用传统RichEdit插入OLE控件实现;另一种是使用CEF实现。这里,我们只讨论RichEdit插入OLE的方案。


 


OLECOMActiveX的关系:


OLE技术以COM规范为基础,OLE技术只是COM规范的一个应用而已。COM是组件程序与客户程序之间进行通信的一种规范。这里有件有趣的事情,虽然OLE是基于COM的,但OLE却早于COM,是不是很费解?其实是这样的:在早期,没有COM,只有OLE,在OLE发展的过程中,产生了COM,或者说,把OLE的规范,抽象出来产生了OLE。这就类似于我们准备写个按钮控件类CMyButton,随着CMyButton的发展,我们发现,可以把CMyButton某些东西提取出来,生成一个控件基类CMyControl,因为以后我们可能要写个从CMyControl派生的CMyComboBox。所以,虽然CMyButton是从CMyControl派生的,但CMyButtonCMyControl出现的早,也是可能的(真实的情况是:OLE 1.0时,组件程序与客户程序之间进行通信是使用的DDE,即动态数据交换机制,到了OLE 2.0时,放弃了DDE,采用了COM)。实际上,COMOLE抽象出来后,确实出现了一系列以COM为基础的新技术,它们统称为ActiveX技术。


 


组件、接口、方法


COM世界中,C++的类,在COM中叫接口,而C++的成员函数,叫接口方法。C++类所在的DLL,叫组件。所以,一个组件可以包含多个接口。


 


下面所讲的方法,无特别说明,都指带句柄的CRichEditCtrl


 


术语:


cp: char position:字符位置索引


格式:无特别说明,这里的格式仅指段落的左缩进、对齐方式、行间距。


一、RichEdit用到的COM接口


    1. IRichEditOle

      IRichEditOleRichEdit中一个非常重要的COM接口,大部分其它接口都与它有直接或间接的关系,很多接口由IRichEditOle查询得到,往RichEdit中插入OLE对象,也是通过这个接口。

       

      IRichEditOle *pRichEditOle = GetIRichEditOle();

       

    2. ITextServices

      RichEdit有两种封装,一种是基于HWND的传统方式,另一种是无句柄的windowless方式,但内部实现,只有一种。那就是基于ITextServices的实现。

      创建windowlessRichedit,其实就是创建Text Services。可由API CreateTextServices完成:

      ITextServices *m_pserv; // 类成员

       

      CComPtr<IUnknown>pUnk = NULL;

      if(CreateTextServices != NULL)

      {

      HRESULThr = CreateTextServices(NULL, this, &pUnk);

      }

       

               HRESULT hr =pUnk->QueryInterface(IID_ITextServices, (void **)&m_pserv);

       

      对于基于HWNDRichEdit,在创建好之后,可以通过IRichEditOle接口查询得到:

      HRESULT hr =pRichEditOle->QueryInterface(IID_ITextServices, (void**)&m_pTextServices);

       

      由于ITextServices接口很常用,可以保存成类成员,不必每次获取。

       

    3. ITextDocument

      ITextDocument接口一般不单独使用。常使用它的Range方法,得到一段范围的ITextRange接口。

       

      通过IRichEditOle接口,可以查询到ITextDocument:

      HRESULT hr =pRichEditOle->QueryInterface(guid, (void **)&m_pTextDocument);

       

    4. ITextRange

      在设置IM控件文本对齐、缩进等格式时,要用到ITextRange。需要注意的是,RangeSelection是不同的概念。Range并不会选择文本,它只是标记了一个范围;而Selection是真实的选中了一段文本,文本背景Highlight加亮显示。Selection是通过SetSel或鼠标框选。而Range是通过ITextDocument::Range方法。我们在代码中,对段落进行格式化,建议少使用SetSel,而应该用Range代替。比如用户使用鼠标,选中了IM中的一段内容,准备复制到剪贴板,但如果使用SetSel选中了其它一个段落准备设置它的段落格式,就会把用户选中的内容清掉,而Range就不会。

       

      通过ITextRange::GetPoint方法,还可以得到指定cp的坐标(四个角的坐标)。但经过测试,只有在RichEdit可视范围内的cp,可以得到坐标。但高版本的RichEdit,支持额外的GetPoint参数(vs2008 MSDN中,并没有额外参数的介绍,在MSDN官网,可以查到),可以得到不可视范围内cp的坐标。还有一种方法,可以得到任意cp的左上角坐标,那就是PosFromChar(GetCharPos,两者内部实现一样)。为了兼容性,建议采用PosFromChar来得到cp坐标。下面是得到cp坐标的详细介绍:


 


RichEdit中指定cp的坐标,有几种方法:


 1.使用PosFromCharGetCharPos(两者内部都是调用EM_POSFROMCHAR),可以得到指定cp左上角坐标。通过指定cp的下一个cp和下一行cp,可以得到指定cp的完整坐标(注意,当整个RichEdit就一个字符时,仍然可以通过PosFromChar(1),得到这个字符右上角坐标,虽然并没有cp1的字符)


 2.通过使用FormatRange,可以模拟输出,从而得到所需要的高度坐标(宽度自己指定)


 3.通过ITextRange::GetPoint,可以得到选中范围完整坐标。它与方法一在微软RichEdit内部都是由CDisplayML::PointFromTp实现。不过使用这个接口有个注意事项,在VS2008sp1本地MSDN介绍中,ITextRange::GetPoint第一个参数只有tomStarttomEndTA_TOPTA_BASELINETA_BOTTOMTA_LEFTTA_CENTERTA_RIGHT的组合。但这样只能得到可视范围内cp的坐标。cp滚动到可见范围外后,得到的是0


 不过。在线的MSDN上,提供了一些额外的参数,可以得到可视范围外的坐标,所有参数如下:


 None                                   0             无选项。


 IncludeInset                     1             将左侧和顶部嵌入添加到矩形的左侧和顶部坐标,并从坐标的右侧和底部除去右侧和底部嵌入。


 Start                                   32             文本范围的开始位置。


 ClientCoordinates         256           返回工作区坐标而不是屏幕坐标。


 AllowOffClient                  512        允许工作区之外的点。


 Transform                         1024      使用宿主应用程序提供的世界转换的变换坐标。


 NoHorizontalScroll        65536    水平滚动已禁用。


 NoVerticalScroll               262144 垂直滚动已禁用。


 所以ITextRange::GetPoint的第一个参数,只要或上512,就可以得到可视区外的坐标了。不过。这些新的属性,不支持旧版本的RichEdit.


 


 


    1. ITextPara

      这个接口,是用来设置指定Range的格式的,包括对齐、缩进、增加行高等。在制作IM控件时,在气泡模式中,对于自己发送的消息,要显示到RichEdit右侧。这个效果,就可以通过对指定Range,调用ITextPara接口,来设置左缩进完成。

      ITextPara接口,由ITextRange:: GetPara返回:

      CComPtr<ITextPara> pTextPara = NULL;

      pTextRange->GetPara(&pTextPara);

      pTextPara->SetIndents(0,GetXFPPTSFromPixel(50), 0);

               pTextRange->SetPara(pTextPara);

       

      这样,就会把Range所在段落的左缩进,设置为50像素。

       

      在使用ITextPara接口时,常常会碰到一个新的坐标单位:floating-point points(简称FPPTS,1 FPPTS等于20分之1缇(twips,而缇与像素的换算关系为:

      X缇等于像素乘以1440再除以DPI值。

      所以,FPPTS与像素的换算关系为:

      X FPPTS等于像素乘以72再除以DPI值。


二、防闪烁


创建一个RichEdit控件,在父窗口缩放带着RichEdit一起缩放时,会明显感觉到内容在闪烁。我们采用常规的防闪烁方案(clip children, WM_ERASEBKGROUD, SetRedraw等)似乎作用不大。由于RichEdit内部是通过ITextServices::TxDraw绘制,所以,我们把RichEdit设置为透明(加上WS_EX_TRANSPARENT扩展风格),然后直接调用TxDraw,把内容画到父窗口上。


 


这里有很多注意事项。下面分别讲述:


  • 要处理WM_PAINT消息,直接return 0


LRESULT CSkinRichEditCtrl::WindowProc(UINTmessage, WPARAM wParam, LPARAM lParam)


{


        if(message == WM_PAINT)


        {


                  //CPaintDC dc(this);不能删除,否则会导致不停的收到WM_PAINT消息


                  CPaintDCdc(this);


                  return0;


        }


        elseif (message == WM_ERASEBKGND)


        {


                  returnTRUE;


        }


 


        returnCRichEditCtrl::WindowProc(message, wParam, lParam);


}


 


  • TxDraw并不是把WM_PAINT的内容显示到另外的DC上。TxDrawWM_PAINT是独立的。RichEdit本身在WM_PAINT中,通过TxDraw绘制到RichEdit上。但你仍然可以自己调用TxDraw,指定与RichEdit不同的宽度,绘制到另外的DC上,两份绘制,呈现的结果是不同的,由于两份绘制传入的宽度不同,会导致显示出来的折行位置不同。并且,即使是自己调用TxDraw,,似乎也会引起滚动位置的重新计算。所以,为了让自己调用的TxDraw显示的内容,与RichEdit内部显示的内容完全吻合。TxDraw的坐标必须传客户区坐标(一定不要传非客户区坐标)。


 


  • RichEdit设置段落格式,一定要在WM_SIZE中处理,不能为了性能考虑,放到绘制的时候。也不能放到WM_VSCROLL中。这是因为,调用pTextRange->SetPara之类的方法设置文本格式,会导致重新计算滚动信息。比如重新设置了行高、设置了缩进等。重新计算滚动信息后,又会导致刷新,这样,在滚动RichEdit时,可能导致按行滚动,而不是按像素滚动。也可能上下跳动。所以,对于RichEdit格式的设置,全部放到插入数据时和WM_SIZE中。


三、气泡


当代的IM聊天控件,大多支持气泡模式。很多人可能认为,气泡、头像应该都是OLE对象。也想过气泡、头像这些元素,可能是画到父窗口上的,作为RichEdit背景透上来的,然后处理RichEdit缩放、滚动等消息,让它们与消息内容连动。大部分人想过之后,会马上下意识的Pass掉方案2,直观认为这种技巧性的方案,不是解决问题的常规方案。然后沿着方案1这个思路往下研究,会发现根本走不通。其实最终解决问题的方案,正是方案2


 


在制作IM软件聊天记录时,对方发送的消息,永远显示到RichEdit左侧,且左对齐。而自己发送的消息,分两种情况:


1.RichEdit宽度小于某个值时,自己发送的消息,显示到右侧。


2.RichEdit宽度大于某个值时,自己发送的消息,显示到左侧,与别人发送的消息一样显示。


不论哪种情况,消息内容,在气泡内都是左对齐的(观察一条消息包含多个段落)。由于消息内容永远是左对齐,当自己发送的消息显示到右侧时,可以通过设置消息段落格式的左缩进,让消息显示到右侧。


 


为了在左右两边显示头像,需要把文字往中间挤,这可以通过调用SetRectRichEdit指定显示区域,为左右两侧留出一些空间。如下图:




 


我们为RichEdit左右各设置40像素的边距,这样,文本就被限制在上图左右红色垂直线之间显示了。因为图像不是RichEdit的内容,而是画到RichEdit父窗口背景上的,所以,这样设置,并不影响头像画到左右缩进线之外。


 


不过这种方式,消息发送者和消息内容,是对齐的,观察QQIM控件,是不对齐的,所以为了以后灵活布局,采用按段落,逐条消息分开设置的方案。


 


在第一章中提到,设置RichEdit的段落格式,一定要放到插入数据时和WM_SIZE中。而获取气泡、头像的坐标,放到显示的时候,这时候,拿到的坐标才是最精确的。在获取气泡、头像坐标时,一定千万不要再设置段落格式


 


3.1内容布局


所谓的内容布局,仅指对消息发送者设置对齐方式和对消息内容进行左缩进设置两项。


 


对于别人发的消息,永远在RichEdit左侧,只需要对消息发送者和消息内容,进行左缩进设置,以留出头像空间。


 


对于自己发的消息,根据是否是气泡模式,且RichEdit的宽度是否大于某个设置的值,会有显示在左侧和右侧两种可能。当显示在左侧时,与别人发的消息同等对待;当显示在右侧时,需要把消息发送者设置成右对齐、为消息内容设置左缩进,从而把消息挤到右侧。


插入时初始布局


为了使消息初始显示的时候,就有正确的布局,需要在插入的时候,就指定好消息的格式。


 


对内容布局,主要是通过ITextPara这个接口。在第一章中有详细介绍。


 


为了布局内容,及计算头像气泡的坐标,需要在插入消息的时候,记录消息插入RichEdit后的一些字符的位置cp。首先,要记录整个消息(从发送者开始)的起始cp,这个cp是相对于整个RichEdit的绝对m_nMsgStartCp。然后记录消息正文开始相对于m_nMsgStartCpcp: m_nMsgContentStartCp。最近记录消息结束位置相对m_nMsgStartCp的结束cp:m_nMsgEndCp。整个消息,只有起始cp是绝对cp。这是因为,IM控件有个功能,就是滚动到最上端后,有个“查看更多消息”。点击之后,会在原有消息之前,再插入一批新的消息,这时候,原有的消息中记录的cp,都要更新。采用上述方法,只更新起始cp即可。


运行时动态布局


在之后运行过程中,当RichEdit尺寸变化后,消息需重新布局。为了减小各种莫名其妙的问题产生,我们只在WM_SIZE中,对消息进行重新布局。


 


对于别人发送的消息,由于永远在左侧显示,在插入的时候,已指定了格式,所以,在插入消息后的任何时间,都不需要再重新设置。所以,所谓的运行时动态布局,仅布局自己发送的消息。


 


由于在RichEdit尺寸发生变化时,不可见的消息内容,也可能发生折行位置cp的改变。所以,需要对所有自己发送的消息,进行格式重置。这需要进行两次循环。


第一次,把所有自己发送的消息的左缩进,重置成消息在左侧显示时的左缩进值。第二次循环,根据当前的设置,计算自己发送的消息占用的宽度,结合RichEdit宽度,计算出消息的左缩进值。


3.2头像、气泡坐标获取


下面讲解得到气泡坐标的算法:


一条消息,可能包含一个或多个段落(以\n分隔),每个段落的文本,也可能很长,也可能很短。比如某条消息,只有一个很短的段落,一行即可显示完整。这样,我们通过《RichEdit杂项.txt》中的方法,很容易计算这段文本的RECT。而如果另一条消息的文本很长,就需要换行显示,如果这条消息还包含多个段落,计算起来就更加复杂了。如果这条消息我们自己发送的,需要显示到RichEdit右侧,那就难上加难了。


我们一步步分析,把复杂问题分解成简单可解的问题。


别人发送的消息,永远显示在左侧,自己发送的消息,根据RichEdit的宽度,可能显示在左侧或右侧。当显示到左侧时,与别人发送的消息同样处理。所以,我们分消息显示到左侧和右侧,分别处理。


消息显示到左侧


我们如何判断所有段落都能在一行显示(即有N个段落,显示N行)完,还是某个或某些段落有换行呢?在插入消息的时候,我们可以查找\n的数量,这个数量+1,就是消息的段落数。把这个段落数,记录到消息结构体CIMMsgm_nMsgParagraphCount成员中。在插入消息的时候,也把消息文本起止位置的cp(char position)分别记录到CIMMsgm_nMsgContentStartCpm_nMsgEndCp成员中。这样,通过LineFromChar就可以得到消息起止字符所在的行的索引。通过行索引差值与m_nMsgParagraphCount比较,就可以知道,是否有某个(些)段落有换行。


 


某个段落有换行


当某个段落有换行时,情况变得比较容易了。因为既然有换行,说明某一行的文字宽度是占满RichEdit文字可显示宽度的,即RichEdit的宽度减左右缩进。这样,我们就得到了气泡坐标的leftright值:


rcQiPao.left =rcInset.left; // rcInsetRichEdit的文本缩进


rcQiPao.right =rcClient.right – rcInset.right; // rcClientRichEdit的坐标


我们只需要得到消息占用的高度即可。通过GetCharPos,可以得到消息第一个字符的左上角坐标,我们只用它的上坐标:


rcQiPao.top =ptFirstChar.y; // ptFirstChar是通过GetCharPos得到的第一个字符的坐标


得到最后一个字符的bottom坐标,方法有些不同。可以通过ITextRange来实现,详见《RichEdit杂项.txt》。


  1. 通过最后一个字符的cp,得到其所在的行

  2. 通过LineIndex得到此行第一个字符的cp

  3. 通过LineLength得到此行的文本长度,从而得到此行最后一个字符的cp

  4. 通过ITextDocument::Range,选中最后一行,返回ITextRange

  5. 通过ITextRange::GetPoint(tomEnd| TA_BOTTOM | TA_RIGHT, &lXEnd, &lYEnd);得到最后一个字符的相对于屏幕的右下坐标CPoint(lXEnd,lYEnd)

  6. 通过调用RichEditScreenToClient,得到相对于RichEdit的坐标。

    这样,气泡的坐标就得到了。

     


当所有段落都能在一行显示(即有N个段落,显示N行)


当所有段落都能在一行显示时,要分别计算每行的rect。然后计算所有rect最大外切矩形。这个外切矩形,就是气泡的坐标。


求每行的rect方法与上面某个段落有换行里介绍的一样。即上面的步骤b-f。可以把它封装成一个接口:


int CWLRichEditCtrl::GetLineRect(intnLineIndex, LPRECT lprcLine, LINE_RECT eLineRect)


消息显示到右侧


某个段落有换行


当某个(些)段落有换行时,情况和显示到左侧完全相同。


当所有段落都能在一行显示(即有N个段落,显示N行)


  1. 先按显示到左侧处理,求到气泡的Rect

  2. 然后用rcClient.Widht() – rcInset.left – rcInset.right – rcQiPao.Width(),求得气泡需要的左缩进量。

     

     

     

     

     

     


杂记:


  1. 当自己发送的消息,需要显示到RichEdit右侧,在计算气泡坐标时。要先设置这条消息的段落格式,左缩进设置为0,计算所需要的宽度和高度。如果所需要的宽度,小于RichEdit的宽度,则重新设置段落的左缩进为“RichEdit的宽度减需要的宽度,达到气泡右侧显示的目的。这里有个注意事项:设置段落格式,可以通过SetParaFormat。前提是要使用SetSel,选中所要设置的段落,但这会导致两个问题:第一个是如果你已经用鼠标选中了RichEdit中的某部分内容,在调用SetSel时,会把你选中的状态清掉。二是在RichEdit窗口左右缩放时,会导致严重的闪烁问题,并且会看到你发的消息,在RichEdit中,左右来回显示(这是因为你一会设置左缩进为0,一会又设置成另一个值)。解决这两个问题的方法是:不使用SetParaFormat,而是使用ITextPara

    CComPtr<ITextDocument> pTextDocument =NULL;

    IMRE_CALL_FUN_RETURN(pTextDocument,GetITextDocument());

     

    ITextRange *pTextRange = NULL;

    pTextDocument->Range(pMsg->GetMsgContentStart(),pMsg->GetMsgEnd(), &pTextRange);

     

    #ifdef _DEBUG

    BSTR bstr;

    pTextRange->GetText(&bstr);

    #endif // _DEBUG

     

    CComPtr<ITextPara> pTextPara = NULL;

    pTextRange->GetPara(&pTextPara);

    pTextPara->SetIndents(0, 0, 0);

    pTextRange->SetPara(pTextPara);

     

    这里,SetIndents的单位是floating-pointpoints,类型为float,可以为正值,也可以为负值。Floating-point points单位与缇的换算公式为:

    #defineTWIPS_TO_FPPTS(x) (((float)(x)) * (float)0.05)

    而像素与缇的换算公式为:

    intLibUIDK::GetXTwipsFromPixel(int nPixel)

    {

          HDC hDCN =::GetDC(::GetDesktopWindow());

          int nXDPI =GetDeviceCaps(hDCN, LOGPIXELSX);

          ::ReleaseDC(::GetDesktopWindow(),hDCN);

     

          if (nXDPI == 0)

          {

                   nXDPI = 96;

          }

     

          int nRet = MulDiv(nPixel,1440, nXDPI);

     

          return nRet;

    }

    所以,像素与floating-point points的换算公式为:

    // 1 FPPTS equalto 1/20 twips

    floatLibUIDK::GetXFPPTSFromPixel(int nPixel)

    {

          HDC hDCN =::GetDC(::GetDesktopWindow());

          int nXDPI =GetDeviceCaps(hDCN, LOGPIXELSX);

          ::ReleaseDC(::GetDesktopWindow(),hDCN);

     

          if (nXDPI == 0)

          {

                   nXDPI = 96;

          }

     

          float nRet =(float)MulDiv(nPixel, 72, nXDPI); // 72 = 1440 * 0.05

     

          return nRet;

    }

     

    只要设置SetIndents的中间参数,就可以设置左缩进。

     

     


四、RichEdit中插入格式化文本


插入格式化文本,有两个重要的结构体:CHARFORMA2TPARAFORMAT2。前者设置字符格式,后者设置段落格式。


typedef struct _charformat2 {

   UINT cbSize; //必须被初始化为sizeof(CHARFORMA2T)

   DWORD dwMask;

   DWORD dwEffects;

   LONG yHeight;

   LONG yOffset;

   COLORREF crTextColor;

   BYTE bCharSet;

   BYTE bPitchAndFamily;

   TCHAR szFaceName[LF_FACESIZE];

   WORD wWeight;

   SHORT sSpacing;

   COLORREF crBackColor;

   LCID lcid;

   DWORD dwReserved;

   SHORT sStyle;

   WORD wKerning;

   BYTE bUnderlineType;

   BYTE bAnimation;

   BYTE bRevAuthor;

   BYTE bReserved1;


} CHARFORMAT2;


 


CHARFORMAT2中的yHeightyOffsetsSpacing的单位是twips()1缇等于1/1440英寸。而1英寸等于DPI像素。DPI的值随操作系统的设置不同而不同,默认是96。它的值可以通过GetDeviceCaps得到,分水平DPI值和垂直DPI值:


        HDChDCN = ::GetDC(::GetDesktopWindow());

        intnXDPI = GetDeviceCaps(hDCN, LOGPIXELSX);

        intnYDPI = GetDeviceCaps(hDCN, LOGPIXELSY);

        ::ReleaseDC(::GetDesktopWindow(),hDCN);


所以给定像素,得到对应多少缇,可以封装成下面的接口:


// 1 twips equal to 1/1440 inch.


int LibUIDK::GetXTwipsFromPixel(int nPixel)

{

        HDChDCN = ::GetDC(::GetDesktopWindow());

        intnXDPI = GetDeviceCaps(hDCN, LOGPIXELSX);

        ::ReleaseDC(::GetDesktopWindow(),hDCN);

        intnRet = nPixel * 1440 / nXDPI;


        returnnRet;

}



int LibUIDK::GetYTwipsFromPixel(int nPixel)

{

        HDChDCN = ::GetDC(::GetDesktopWindow());

        intnYDPI = GetDeviceCaps(hDCN, LOGPIXELSY);

        ::ReleaseDC(::GetDesktopWindow(),hDCN);

        intnRet = nPixel * 1440 / nYDPI;


        returnnRet;

}



  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
libUIDK是一款基于MFC的用户界面,用于开发Windows平台的应用程序。它提供了丰富的基本控件,可以满足各种应用程序的需求。 基本控件使用非常简单,以下是一些常用的基本控件及其使用方法: 1.按钮(Button):通过在对话框上拖放按钮控件,可以轻松创建按钮。可以设置按钮的文本、图标、大小、位置等属性,还可以为按钮添加响应事件,实现按钮的点击功能。 2.文本框(Edit):文本框用于输入文本或显示文本内容。通过在对话框上拖放文本框控件,并设置其属性,可以实现对文本框的定制。可以设置文本框的大小、位置、提示文本、只读属性等。 3.标签(Static):标签用于显示文本或图标。通过在对话框上拖放标签控件,并设置其属性,可以实现对标签的定制。可以设置标签的文本、字体、大小、位置等。 4.列表框(List Box):列表框用于显示多个选项,并允许用户从中选择一个或多个选项。通过在对话框上拖放列表框控件,并添加选项,可以实现对列表框的定制。可以设置列表框的大小、位置、选项等。 5.组合框(Combo Box):组合框是一个下拉列表框,可以显示多个选项,并允许用户从中选择一个选项,也可以自由输入文本。通过在对话框上拖放组合框控件,并添加选项,可以实现对组合框的定制。可以设置组合框的大小、位置、选项等。 总之,libUIDK提供了丰富的基本控件,可以通过在对话框上拖放控件、设置属性、添加响应事件等方式来使用。这些基本控件能够满足各种应用程序的需求,帮助开发者轻松构建用户友好的界面

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值