DirectUI框架GUIFW

2023-05-16

 

前言

guifw是一款基于GDI+的DirectUI皮肤引擎,借鉴了DuiLib和Qt的思想

效果预览:http://download.csdn.net/detail/sllins/7707771 

代码已开源:https://github.com/arlins/GUIFW



guifw实现了xml动态创建,实现了UI和逻辑的分离,支持换肤等,基于guifw所搭建的框架可以很容易派生自定义控件,目前实现了常用的button,image,label等控件,由于时间关系,没有对其他控件进行完善。下面对guifw的整个框架进行大概的总结。

所谓DirectUI(Paint on parent dc directly)就是直接在父窗口上绘图,也即直接在DC上面画一个窗口里面的所有控件,最终整个窗口是一张图。所有的DirectUI最基本框架的无外乎就是动态创建,事件派发,绘制引擎和UI布局这四部分组成,guifw也是如此,对于guifw来说,所有的窗口都从GuiFrameWindow派生,而所有的控件都是从GuiWidget派生,窗口里面的内容都由GuiWidget组成。GuiFrameWindow和GuiWidget都派生自GuiObject,一个继承自GuiObject的对象只要有Parent,那么会在Parent删除的时候一并被删除。下面对整个框架进行介绍

 

 一、框架简介

 

上图是guifw的框架图,其中底层是core模块,用于为上层和外部提供最基本的支持,基于core模块可以扩展各种各样的控件,对话框等,最终形成一个完整的皮肤库。
1、 xmlui主要实现了动态创建功能
2、 layout实现了对GuiFrameWindow里面所有的GuiWidget的逻辑排版,支持HBox和VBox
3、 application是整个程序的入口,程序所有消息都经过这里,再派发给相应的窗口
4、 skinmanager实现换肤功能以及换肤广播工作
5、 window是所有窗口的基类,也即GuiFrameWindow,GuiframeWindow管理着所有的GuiWidget,负责将绘制以及事件派发给相应的GuiWidget等工作
6、 widget是每个窗口里面最基本的元素,也即GuiWidget,一个窗口的界面由若干个widget组成,所有的控件都由它派生,整个界面数据结构是由一颗每个节点是widget组成的树。
7、 painter跟窗口有关,每个窗口都有一个painter,在需要绘制的时候将painter传给需要重绘区域里面的所有widget,guifw里面采用的绘图引擎是gdi+。实际上painter就是封装了对一个窗口相关的Bitmap的绘图操作,所有的widget都在这张bitmap上面画图,最终绘制出来一个窗口就是一张图。
8、 eventdispatcher是事件派发者,包括application将事件派发给所有的窗口以及窗口派发事件给需要响应的widget两部分。

 

下面对guifw进行更深入的介绍。

 

二、动态创建

动态创建要解决的就是如何根据类名创建一个类,我们很容易想到switch/case语句,DuiLib就是这么做的。但这么做是最简单扩展性极差的办法,如果新增加多一种类型就必须在case语句加上这种类型并且创建相应的实例,如果我们开发的皮肤库是以dll的方式提供给第三方使用者而不是源码,那就完蛋了,根本无法自定义控件。

其实只要想到将类名和类似getInstance这样创建该类对象的函数注册进一个map里面,当我们需要创建某个类的实例的时候就去这个map里面找,找到了就调用相应的getInstance创建实例即可,这个问题就解决了,这个事情就是xmlui所做的一部分工作,xmlui还将创建整颗界面树,解析和设置widget的属性等等

//用于动态创建的函数定义
typedef GuiWidget* (* pGuiWidgetCreateFunc)(GuiWidget *parent);

//动态创建的宏
#define GUI_DECLARE_DYNAMIC_CREATE_SELF(classname,parenttype,functype) \
public:\
 static parenttype* dynamicCreateObject##classname(parenttype* parent) \
 { \
  classname* p = new classname(parent); \
  return static_cast<parenttype*>(p);\
 } \
private: \
 static TypeNode<functype> s_register##classname;

#define GUI_IMPLEMENT_DYNAMIC_CREATE_SELF( classname, functype ) \
 TypeNode<functype> classname::s_register##classname=TypeNode<functype>( L#classname, classname::dynamicCreateObject##classname );

//widget动态创建宏
#define GUI_DECLARE_WIDGET_DYNAMIC_CREATE_SELF( classname ) \
 GUI_DECLARE_DYNAMIC_CREATE_SELF( classname, GuiWidget, pGuiWidgetCreateFunc );

#define GUI_IMPLEMENT_WIDGET_DYNAMIC_CREATE_SELF( classname ) \
 GUI_IMPLEMENT_DYNAMIC_CREATE_SELF( classname, pGuiWidgetCreateFunc );

 

这里省略了列表定义以及将classname,functype这个对应关系插入map的操作。这样定义之后只要在需要支持动态创建的类声明里面添加GUI_DECLARE_WIDGET_DYNAMIC_CREATE_SELF这个声明,再在类实现添加GUI_IMPLEMENT_WIDGET_DYNAMIC_CREATE_SELF定义就可以了,这样动态创建功能就实现了,关于属性方面就没什么好说的了,无非就是解析xml然后进行相关设置。


三、事件派发
程序消息由application接收,再将事件转给相应窗口的窗口过程,窗口过程将消息细分后转给对应的窗口,窗口再将消息转给需要响应的widget。

1、 Application转发消息给相应窗口,其中eventFilterFunc是给外部一个机会过滤消息

MSG msg = { 0 };
while( ::GetMessage(&msg, NULL, 0, 0) ) 
{
	bool bMsgFlitered = false;
	if ( d_ptr->eventFilterFunc )
	{
		bMsgFlitered = (*(d_ptr->eventFilterFunc))( & msg ) ;
	}
	if( !bMsgFlitered ) 
	{
		::TranslateMessage(&msg);
		::DispatchMessage(&msg);
	}
}

2、 窗口过程将消息细分后转给对应的窗口,windoweventdispatcher就是干这个事情的

LRESULT CALLBACK GuiWindowEventDispatcher::WinProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
	GuiFrameWindow* pData = (GuiFrameWindow*)::GetWindowLong( hWnd, GWL_USERDATA );
	if ( pData == NULL || pData->hwnd() != hWnd )
	{
		return ::DefWindowProc( hWnd, uMsg, wParam, lParam );
	}

	pData->winEvent( &ev );
	switch( uMsg ) 
	{
		case WM_NCHITTEST:
			{
				LRESULT lrs = pData->windowNcHitTest( &ev );
				return lrs;
			}
			break;
		case WM_SIZE: 
			{
				pData->windowResizeEvent( &ev ); 
				return FALSE ;
			}
			break;
		case WM_LBUTTONDOWN: 
			{
				pData->windowMouseLButtonPressEvent( &ev ); 
				return FALSE;
			}
			break;
		case WM_LBUTTONUP: 
			{
				pData->windowMouseLButtonReleaseEvent( &ev ); 
				return FALSE;
			}
			break;
		case GuiPaint:
			{
				pData->windowPaintEvent();
				return FALSE;
			}
			break;
		//…
		default:
			break;
	}

	return ::DefWindowProc( hWnd, uMsg, wParam, lParam );
}

3、 窗口再将消息转给需要响应的widge,通过widgeteventdispatcher完成,也就是说,每个窗口都有一个widgeteventdispatcher成员,负责将窗口消息转给widget。

//eg01
void GuiFrameWindow::windowMouseLButtonPressEvent(GuiEvent* msg)
{
      mouseLButtonPressEvent( msg);
      d_ptr->wigetEventDispatcher->mouseLButtonPressEvent( msg);
}
 
void GuiWidgetEventDispatcher::mouseLButtonPressEvent(GuiEvent* msg)
{
      Point pt = d_ptr->frameWindow->clientPointFromEvent( msg);
      GuiWidget* newClickWidget= d_ptr->frameWindow->findWidgetByPoint( pt);
      if ( newClickWidget!= NULL )
      {
           newClickWidget->mouseLButtonPressEvent(msg );
      }
      d_ptr->oldClickWidget= newClickWidget;
}
 
//eg02
void GuiFrameWindow::windowResizeEvent(GuiEvent* msg)
{
      resizeEvent( msg);
      d_ptr->wigetEventDispatcher->resizeEvent( msg);
}
void GuiWidgetEventDispatcher::resizeEvent(GuiEvent* msg)
{
      // DispatchEventToWidgets是一个宏定义,他会分层遍历整颗界面树然后调用每个widget的resizeEvent函数
      DispatchEventToWidgets( resizeEvent,msg );
}

4、 走到了第三步,widget事件处理的问题就很容易了,例如一个按钮,有多种状态,我们只需要在各个事件发生的时候设置正确的状态,然后调用update即可。目前guifw已经实现了大部分事件的派发。

protected:
      friend class GuiWidgetEventDispatcher;
      virtual void construct();
      virtual void paintEvent( GuiPainter*painter );
      virtual void closeEvent( GuiEvent*msg );
      virtual void resizeEvent( GuiEvent*msg );
      virtual void hoverMoveEvent( GuiEvent*msg );
      virtual void hoverEnterEvent( GuiEvent*msg );
      virtual void hoverLeaveEvent( GuiEvent*msg );
      virtual void mouseLButtonPressEvent( GuiEvent*msg );
      virtual void mouseLButtonReleaseEvent( GuiEvent*msg );
      //...

事件派发的比较麻烦的地方在于怎么合理处理某些事件的过渡,例如HoverEnter和HoverLeave和HoverMove,再例如先在其他地方按下鼠标,再将鼠标移到按钮上面放开,这个时候按钮不应该收到release消息的,这些问题如需要妥当处理否则可能导致消息派发错

 

四、绘图引擎

根据上面搭建的widget结构树,绘图基本上已经不成问题了,最简单的做法是在需要绘制的时候分层遍历整棵树,然后一次性把这张图贴到窗口上,但很显然,我们不可能为了更新一个按钮区域而刷新整个窗口,这个效率太差了,所以需要对此进行改进,两个办法:设置“脏区域”(也即无效区域)以及设置裁剪区域,,在绘制的时候设置裁剪区域并且只绘制“脏区域”内的widget,Qt和DuiLib都是这么做的。
可能在事件派发的时候你已经留意到guifw在绘制的时候使用的是GuiPaint 自定义消息然后调用window的windowPaintEvent去进行绘制,这是因为guifw最终使用的是UpdateLayeredWindow去绘制窗口,目前guifw没有实现用BitBlt去绘制,更高效的做法应该是支持两种情况,当没有透明度的时候我们就用BitBlt,当需要支持透明的时候就用UpdateLayeredWindow,Qt就是这样实现的,DuiLib就只用了BitBlt,所以这个就意味着DuiLib不支持背景半透明这个局限。回到刚才的问题,因为使用了UpdateLayeredWindow所以就没有WM_PAINT消息了,也就是说guifw自己接管了绘制工作,而不依赖于系统去绘制。这会带来另外一个问题是我们刚才说设置脏区域,UpdateLayeredWindow不能像BitBlt那样可以局部刷新,窗口无效区域只对WM_PAINT有效,所以我们需要自己管理无效区域,这个并不麻烦,只需要在窗口内保存一个Rect标识无效区域,在绘制的时候只绘制无效区域内的widget即可。
具体操作如下:

1、在绘制前已经设置好了无效区域,需要注意的是无效区域必须合并

void GuiFrameWindow::update(Rect dirtyRect)
{
      if ( dirtyRect.IsEmptyArea() )
      {
           d_ptr->painter->updatePainter();
           d_ptr->dirtyRect= rect();
      }
      else
      {
           if ( d_ptr->dirtyRect.IsEmptyArea())
           {
                 d_ptr->dirtyRect= dirtyRect;
           
           else
           {
                 Rect::Union( d_ptr->dirtyRect,dirtyRect, d_ptr->dirtyRect );
           }
      }
 
      _postMessage( GuiPaint,0, 0 ); //通知窗口绘图
}

2、前面我们说到当程序收到GuiPaint的反应是调用窗口的windowPaintEvent函数进行绘图
3、绘图前先设置裁剪区域,之后先画背景(如果有)
4、紧跟着画所有在无效区域内可见的widget

d_ptr->wigetEventDispatcher->paintEvent(d_ptr->painter);

前面我们已经知道wigetEventDispatcher是干嘛的了,我们再看看wigetEventDispatcher里面的paintEvent实现。

void GuiWidgetEventDispatcher::paintEvent(GuiPainter* painter)
{
      if ( d_ptr->frameWindow == NULL)
      {
           return;
      }
 
      std::deque<GuiObject*> dequeNode;
      dequeNode.push_back(static_cast<GuiObject*>(d_ptr->frameWindow->rootWidget() ) );
      while( !dequeNode.empty() )
      {
           GuiObject *pItem=dequeNode.front();
           dequeNode.pop_front();
           GuiWidget* itemWidget= static_cast<GuiWidget*>( pItem );
           if ( itemWidget&& itemWidget->isVisible() ) //如果widget可见
           {
                 bool widgetInDirtyRect= ( itemWidget->geometry().Intersect( d_ptr->frameWindow->dirtyRect()) == TRUE );
                 if ( widgetInDirtyRect)  //如果widget在无效区域内才画
                 {
                      itemWidget->paintEvent(painter );
                 }
           }
           for ( std::list<GuiObject*>::iterator it = pItem->getChildren().begin(); it != pItem->getChildren().end(); it++ )
           {
                 dequeNode.push_back(*it );
           }
      }
}

paintEvent实际上干的事情只有一个,那就是是分层遍历整颗widget树,将可见并且在无效区域内的widget进行绘制,分层遍历为的是先调用父亲的paint然后再调用孩子的paint,也可以理解为这个是widget之间的z轴关系。

前面提到painter说是封装了对一个窗口相关的Bitmap的绘图操作,所有的widget都在这张bitmap上面画图,最终绘制出来一个窗口就是一张图。下面看看painter的一些实现。

void GuiPainterPrivate::updatePainter()
{
       if ( frameWindow== NULL )
       {
              return;
       }
 
       clearPainter();
       memoryBitmap = newBitmap( frameWindow->width(), frameWindow->height() );
       graphics = Graphics::FromImage( memoryBitmap);
}

在窗口resize的时候就调用updatePainter,updatePainter干的事情是更新一张和window尺寸一样的Bitmap,在窗口大小没有发生改变的情况下,所有的绘图操作都在这张Bitmap上面完成,举个例子,例如绘制一张图,可以如下操作

void GuiPainter::drawImage(const std::wstring& imagePath,const Rect&rc )
{
       if ( d_ptr->graphics == NULL)
       {
              return;
       }
 
       Image image( imagePath.c_str());
       d_ptr->graphics->DrawImage( &image, rc );
}

这样就实现了在Bitmap上面绘制一张图的目的,其他DrawText等也就类似的操作了。
这就是为什么要把painter传给每个widget的原因,所有的widget都共用一个painter以及painter提供的drawImage,drawText函数,通过共用一个painter也就是实现了所有widget的绘图操作都在同一个Bitmap上面进行。


5、最后将整个内存位图一次性贴到窗口上

//完整的windowPaintEvent实现代码如下:
void GuiFrameWindow::windowPaintEvent()
{
      if ( !isVaildWindow())
      {
           return;
      }  
 
      //设置裁剪区域
      d_ptr->painter->setClipRect( d_ptr->dirtyRect, CombineModeReplace);
      if ( !d_ptr->bTransparentBackground )
      {
           drawBackground( d_ptr->painter );
      }
 
      paintEvent( d_ptr->painter );
      //画所有的widget
      d_ptr->wigetEventDispatcher->paintEvent( d_ptr->painter );
 
      //将整张图一次性贴到DC上
      PAINTSTRUCT ps= { 0 };
      HDC hdc = BeginPaint( hwnd(),&ps); 
      HDC hMemDc = CreateCompatibleDC( hdc);  // 创建与当前DC兼容的内存DC   
 
      BYTE * pBits ;
      BITMAPINFOHEADER bmih;
      ZeroMemory( &bmih,sizeof( BITMAPINFOHEADER) );
      bmih.biSize = sizeof (BITMAPINFOHEADER);
      bmih.biWidth = width() ;
      bmih.biHeight =height() ;
      bmih.biPlanes =1 ;
      bmih.biBitCount= 32;
      bmih.biCompression= BI_RGB ;
      bmih.biSizeImage= 0 ;
      bmih.biXPelsPerMeter= 0 ;
      bmih.biYPelsPerMeter= 0 ;
      bmih.biClrUsed= 0 ;
      bmih.biClrImportant= 0 ;
      HBITMAP hBmpBuffer= CreateDIBSection(NULL,(BITMAPINFO *) &bmih,0, (VOID**)&pBits,NULL, 0) ;// 创建一块指定大小的位图
      HGDIOBJ hPreBmp= SelectObject( hMemDc,hBmpBuffer );  // 将该位图选入到内存DC中,默认是全黑色的  
 
      Image* bitmap =d_ptr->painter->memoryBitmap();
      Graphics graph(hMemDc );
      graph.DrawImage(bitmap, 0, 0, 0, 0, (INT)bitmap->GetWidth(), (INT)bitmap->GetHeight(),UnitPixel );
      graph.ReleaseHDC(hMemDc );
 
      POINT ptWinPos={geometry().GetLeft(),geometry().GetTop()};
      SIZE sizeWindow={width(),height()};
      POINT ptSrc={0,0};
      DWORD dwExStyle= ::GetWindowLong( hwnd(),GWL_EXSTYLE );
      if ( (dwExStyle&0x80000)!= 0x80000 )
      {
           ::SetWindowLong( hwnd(),GWL_EXSTYLE,dwExStyle^0x80000);
      }
 
      ::UpdateLayeredWindow( hwnd(),hdc, &ptWinPos,&sizeWindow, hMemDc,&ptSrc, 0, &d_ptr->blend, ULW_ALPHA );
 
      SelectObject( hMemDc,hPreBmp ); 
      DeleteObject( hBmpBuffer); 
      DeleteDC( hMemDc);
      EndPaint( hwnd(),&ps );
     
      _setWindowCorner(); //设置窗口圆角
      d_ptr->dirtyRect= Rect(0,0,0,0); //更新无效区域
}

通过这样我们就完成了整个窗口的绘制,这样我们的界面就能顺利绘制出来了。

 

五、UI布局
guifw排版和绘制是分开的,并不像duilib一样把绘制和排版混在一起,在绘制之前先排版。目前支持两种基本的布局方式:HBox和VBox,每个widget如果有孩子那么一定要设置布局方式。

void GuiWidget::setLayout(const std::wstring& value)
{    
       if ( value == L"HBox")
       {
              setLayout( HBox);
       }
       else if ( value == L"VBox")
       {
              setLayout( VBox);
       }
}

然后widget通过在设置位置或者大小的时候widget会自动调用updateLayout进行排版

void GuiWidget::updateLayout()
{
       if ( d_ptr->layoutType == HBox)
       {
              GuiWidgetHBoxLayout::updateLayout(this );
       }
       else if ( d_ptr->layoutType== VBox )
       {
              GuiWidgetVBoxLayout::updateLayout(this );
       }
}

updateLayout函数实现布局目前只是进行了比较简单计算操作,如果要扩展布局方式也很简单,只需要只增加多一种类型,然后调用相应的排版函数进行排版,updateLayout里面要支持多种属性(例如padding等)都很方便,只不过是排版算法的问题。

 

六、二进制兼容

如果在开发皮肤库的时候是以dll方式提供给第三方使用者而不是源码,那么考虑二进制兼容很有必要,Qt的做法非常值得借鉴,guifw也是学习了Qt的这种做法,采用私有类指针,这样成员变量保存在私有类里面,成员变量的改动不会破坏二进制兼容,当然如果改动成员函数那就没办法了。

 

七、其他

在做guifw之前其实可以设计得更加灵活,例如绘图引擎可以定义一个接口类,再实现一个对gdi+进行封装的绘图引擎,这样如果以后有需要换其他方式绘图引擎只需要实现引擎接口然后把新引擎注册进去就可以,这是Qt的做法,但由于这样做需要自己封装Rect,Point,Image等等常用的类型,guifw里面使用的是gdi+的这些类型,所以无法将gdi+做到彻底隔离。

 

八、写在后面的话

guifw开发前前后后用了差不多一个月的时间,框架还是比较简陋,可优化的地方还有很多,通过实践这个项目,对自己的提升还是很有帮助,duilib估计大家比较熟悉,如果你没接触过Qt,我强烈建议你去看一下,因为里面可以学习的东西太多了,甚至你可以基于Qt去写一个皮肤库,绘图引擎Qt已经帮里做了,事件派发Qt也已经帮你做了,布局也帮你做了一些,你需要做的,只是组织一下布局以及动态创建,一个皮肤库就出来了。

参考资料:
1:http://www.viksoe.dk/code/windowless1.htm
2:http://code.google.com/p/duilib/
3:http://qt-project.org/

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

DirectUI框架GUIFW 的相关文章

随机推荐

  • 用zt-zip Java库进行zip文件处理

    Java 标准库本身自带java util zip包 xff0c 利用该包可以解决zip文件的处理问题 但是该包提供的功能相对底层 xff0c 想要实现zip文件的处理 xff0c 需要写一 些代码 xff0c 该包并没有封装API到调用一
  • Linux临时目录/tmp与/var/tmp

    Linux有两个公知的临时目录 xff1a tmp与 var tmp xff0c 这两个目录被用户用于存储临时性的文件 xff0c 亦经常被程读写用户存储临时性数据 两个目录没有本质上的区别 xff0c 最根本的区别仅仅是系统对其中文件清理
  • Python 日志打印

    核心概念 Python标准库自带日志模块logging xff0c logging中涉及到4个核心组件 xff0c 这些组件构建了logging体系 Logger xff1a 应用程序直接使用的接口对象 xff0c 通过logger操作完成
  • Docker指定网桥和指定网桥IP

    docker network ls NETWORK ID NAME DRIVER 7fca4eb8c647 bridge bridge 9f904ee27bf5 none null cf03ee007fb4 host host Bridge
  • Python Hash操作-MD5-SHA-HMAC

    Python标准库提供了计算数据Hash的功能 xff0c 支持许多不同的算法 xff0c 常见的MD5 SHA1 SHA256 HMAC均在其中 MD5 SHA1 SHA256在hashlib模块中 xff0c HMAC在hmac模块中
  • 离线环境下火狐浏览器Firefox完全信息迁移

    火狐浏览器Firefox是一个历史比较久的网页浏览器 xff0c 当前的火狐采用顺序数字命名的版本号演进 xff0c 迭代速度较快 xff0c Windows上可以自动升级 xff0c 升级带来了安全更新和功能特性上的变化 火狐浏览器具书签
  • 用SLF4J输出log的正确姿势

    slf4j是Java的一种Log Api xff0c 类似Apache Commons Logging 最直接的log方式 logger debug 34 Entry number 34 43 i 43 34 is 34 43 String
  • Thymeleaf消息表达式

    消息表达式用于从消息源中提取消息内容实现国际化 表达式的语法 xff1a span class token tag span class token tag span class token punctuation lt span p sp
  • Thymeleaf URL表达式

    URL在Thymleaf中是第一类公民 xff0c 有其专有的表达式语法 64 共存在2大类URL xff1a 绝对URL http www your domain相对URL xff0c 分为四类相对于页面 user login html相
  • Maven resources的include和exclude

    Maven resources plugin支持明确声明 lt directory gt 指定的资源目录中哪些资源需要处理 xff0c 哪些资源可以不被处理 lt include gt 指明需要包括的资源 xff0c 位于src my re
  • Thymeleaf条件判断

    th if th if属性求Bool值 xff0c 只有true的时候其所在的标签及该标签中的内容才会被渲染到输出结果中 lt a href 61 34 comments html 34 th href 61 34 64 product c
  • 2019 蓝桥杯省赛 A 组模拟赛(一)C. 结果填空:马的管辖 (暴力搜索)

    题目 xff1a 在中国象棋中 xff0c 马是走日字的 一个马的管辖范围指的是当前位置以及一步之内能走到的位置 xff0c 下图的绿色旗子表示马能走到的位置 如果一匹马的某个方向被蹩马脚 xff0c 它就不能往这个方向跳了 xff0c 如
  • Ubuntu下安装使用Xfce4

    安装 xff1a 代码 sudo apt get install xfce4 xfce4 taskbar plugin xfce4 taskbar plugin是我需要 xff0c 你可不用 xff0c 完整安装xfce4 的桌面环境 su
  • isdigit()函数如何判断负数

    在使用字符序列isdigt函数时 xff0c 我们会发现它无法判断负数 xff0c 如 xff1a a 61 39 2 39 39 3 39 4 5 print a 0 isdigit print a 1 isdigit 输出 xff1a
  • 用python操作浏览器的三种方式

    第一种 xff1a selenium导入浏览器驱动 xff0c 用get方法打开浏览器 xff0c 例如 xff1a import time from selenium import webdriver def mac driver 61
  • Linux-虚拟网络设备-veth pair

    基本概念 Virtual Ethernet CableBidirectional FIFOOften used to cross namespaces Linux container 中用到一个叫做veth的东西 xff0c 这是一种新的设
  • openstack-neutron-OVS agent(持续更新)

    概述 ML2Plugin的主要工作是管理虚拟网络资源 xff0c 保证数据正确无误 xff0c 具体物理设备的设置则由Agent完成 L2Agent通常运行在Hypervisor xff0c 与neutron server通过RPC通信 x
  • VUE中使用EventSource接收服务器推送事件

    Vue项目中 xff0c EventSource触发的事件中this指向变了 使用const that 61 this xff0c 然后在EventSource触发的事件中使用that if typeof EventSource 61 61
  • VNC

    一 安装tigervnc server 二 配置登录帐号 三 生成xstartup与log日志 注意 xff1a 如果没有使用vncserver来 设置密码 xff0c 则service vncserver restart 是不会成功的 这
  • DirectUI框架GUIFW

    前言 guifw是一款基于GDI 43 的DirectUI皮肤引擎 xff0c 借鉴了DuiLib和Qt的思想 效果预览 xff1a http download csdn net detail sllins 7707771 代码已开源 xf