文章出處

CSharpGL(45)自制控件的思路

+BIT祝威+悄悄在此留下版了個權的信息說:

本文介紹CSharpGL實現自制控件的方法。

所謂自制控件,就是用純OpenGL模仿WinForm里的Button、Label、TextBox、CheckBox等控件,支持布局、修改大小和文字等功能。

 

如上圖所示,左下角就是一個顯示二維圖片的類似PictureBox的控件,我稱之為CtrlImage。(所有的CSharpGL自制控件類型,都繼承自GLControl,都添加前綴Ctrl)CtrlImage上方分別是一個CtrlButton和一個CtrlLabel。

下載

CSharpGL已在GitHub開源,歡迎對OpenGL有興趣的同學加入(https://github.com/bitzhuwei/CSharpGL

 

控件是什么?

一個控件,最基本的屬性包括這幾條:隸屬關系(Parent、Children),布局屬性(Location、Size、Anchor),渲染(Initialize、Render、BackgroundColor等),事件(Click、Resize等)。

+BIT祝威+悄悄在此留下版了個權的信息說:

隸屬關系

WinForm里的控件們構成了一個形關系,CSharpGL也是這樣。有了這樣的隸屬關系,就可以以相對于Parent的位置來記錄自己的位置。

而且,當我做好了CtrlLabel,就可以直接放到CtrlButton.Children里,于是CtrlButton上顯示文字的功能就瞬間實現了(當然還要設置一下文字位置,但工作量已經可以忽略不計了)。

 1 using System;
 2 
 3 namespace CSharpGL
 4 {
 5     /// <summary>
 6     /// Control(widget) in OpenGL window.
 7     /// </summary>
 8     public abstract partial class GLControl
 9     {
10         internal GLControl parent;
11         [Description("Parent control. This node inherits parent's layout properties.")]
12         public GLControl Parent
13         {
14             get { return this.parent; }
15             set
16             {
17                 GLControl old = this.parent;
18                 if (old != value)
19                 {
20                     this.parent = value;
21 
22                     if (value == null) // parent != null
23                     {
24                         old.Children.Remove(this);
25                     }
26                     else // value != null && parent == null
27                     {
28                         value.Children.Add(this);
29                     }
30                 }
31             }
32         }
33 
34         [Description("Children Nodes. Inherits this node's IWorldSpace properties.")]
35         public GLControlChildren Children { get; private set; }
36 
37         public GLControl(GUIAnchorStyles anchor)
38         {
39             this.Children = new GLControlChildren(this);
40 
41             this.Anchor = anchor;
42         }
43     }
44 }
View Code
+BIT祝威+悄悄在此留下版了個權的信息說:

布局屬性

首先要有Location和Size。然后,在Parent的Size改變時,自己要相應的改變Location和Size,那么就需要Anchor來指定“是不是維持與某一邊的距離不變”。

如何計算自己更新后的Location和Size?這是個簡單的算法問題。

  1 using System;
  2 
  3 namespace CSharpGL
  4 {
  5     public partial class GLControl
  6     {
  7         /// <summary>
  8         /// 獲取或設置控件綁定到的容器的邊緣并確定控件如何隨其父級一起調整大小。
  9         /// </summary>
 10         public GUIAnchorStyles Anchor { get; set; }
 11 
 12         private int x;
 13         private int y;
 14 
 15         /// <summary>
 16         /// 相對于Parent左下角的位置(Left Down location)
 17         /// </summary>
 18         public GUIPoint Location
 19         {
 20             get { return new GUIPoint(x, y); }
 21             set { this.x = value.X; this.y = value.Y; }
 22         }
 23 
 24         public GUISize Size
 25         {
 26             get { return new GUISize(width, height); }
 27             set { this.width = value.Width; this.height = value.Height; }
 28         }
 29 
 30         private int width;
 31         private int height;
 32 
 33         /// <summary>
 34         /// 上次更新之后,parent的Width屬性值。
 35         /// </summary>
 36         private int parentLastWidth;
 37         /// <summary>
 38         /// 上次更新之后,parent的Height屬性值。
 39         /// </summary>
 40         private int parentLastHeight;
 41 
 42         /// <summary>
 43         /// 
 44         /// </summary>
 45         protected int absLeft;
 46         /// <summary>
 47         /// 
 48         /// </summary>
 49         protected int absBottom;
 50 
 51         /// <summary>
 52         /// Layout for this control.
 53         /// </summary>
 54         public virtual void UpdateAbsoluteLocation()
 55         {
 56             GLControl parent = this.Parent;
 57             if (parent != null)
 58             {
 59                 this.absLeft = parent.absLeft + this.x;
 60                 this.absBottom = parent.absBottom + this.y;
 61             }
 62             else
 63             {
 64                 this.absLeft = this.x;
 65                 this.absBottom = this.y;
 66             }
 67         }
 68 
 69         /// <summary>
 70         /// layout controls in OpenGL canvas.(
 71         /// Updates absolute and relative (location and size) of specified node and its children nodes.
 72         /// <para>This coordinate system is shown as below.</para>
 73         /// <para>   /\ y</para>
 74         /// <para>   |</para>
 75         /// <para>   |</para>
 76         /// <para>   |</para>
 77         /// <para>   |</para>
 78         /// <para>   |</para>
 79         /// <para>   |-----------------&gt;x</para>
 80         /// <para>(0, 0)</para>
 81         /// </summary>
 82         /// <param name="node"></param>
 83         public static void Layout(GLControl node)
 84         {
 85             if (node == null) { return; }
 86 
 87             var parent = node.Parent;
 88             if (parent != null)
 89             {
 90                 NonRootNodeLayout(node, parent);
 91             }
 92 
 93             node.UpdateAbsoluteLocation();
 94 
 95             foreach (var item in node.Children)
 96             {
 97                 GLControl.Layout(item);
 98             }
 99 
100             if (parent != null)
101             {
102                 node.parentLastWidth = parent.width;
103                 node.parentLastHeight = parent.height;
104             }
105         }
106 
107         private const GUIAnchorStyles leftRightAnchor = (GUIAnchorStyles.Left | GUIAnchorStyles.Right);
108         private const GUIAnchorStyles topBottomAnchor = (GUIAnchorStyles.Top | GUIAnchorStyles.Bottom);
109 
110         /// <summary>
111         /// Updates <paramref name="currentNode"/>'s location and size according to its state and parent's information.
112         /// </summary>
113         /// <param name="currentNode"></param>
114         /// <param name="parent"></param>
115         private static void NonRootNodeLayout(GLControl currentNode, GLControl parent)
116         {
117             int x, y, width, height;
118             if ((currentNode.Anchor & leftRightAnchor) == leftRightAnchor)
119             {
120                 width = parent.width - currentNode.parentLastWidth + currentNode.width;
121                 if (width < 0) { width = 0; }
122             }
123             else
124             {
125                 width = currentNode.width;
126             }
127 
128             if ((currentNode.Anchor & topBottomAnchor) == topBottomAnchor)
129             {
130                 height = parent.height - currentNode.parentLastHeight + currentNode.height;
131                 if (height < 0) { height = 0; }
132             }
133             else
134             {
135                 height = currentNode.height;
136             }
137 
138             if ((currentNode.Anchor & leftRightAnchor) == GUIAnchorStyles.None)
139             {
140                 int diff = parent.width - currentNode.parentLastWidth;
141                 x = currentNode.x + diff / 2;
142             }
143             else if ((currentNode.Anchor & leftRightAnchor) == GUIAnchorStyles.Left)
144             {
145                 x = currentNode.x;
146             }
147             else if ((currentNode.Anchor & leftRightAnchor) == GUIAnchorStyles.Right)
148             {
149                 int diff = parent.width - currentNode.parentLastWidth;
150                 x = currentNode.x + diff;
151             }
152             else if ((currentNode.Anchor & leftRightAnchor) == leftRightAnchor)
153             {
154                 x = currentNode.x;
155             }
156             else
157             { throw new Exception(string.Format("Not expected Anchor:[{0}]!", currentNode.Anchor)); }
158 
159             if ((currentNode.Anchor & topBottomAnchor) == GUIAnchorStyles.None)
160             {
161                 int diff = parent.height - currentNode.parentLastHeight;
162                 y = currentNode.y + diff / 2;
163             }
164             else if ((currentNode.Anchor & topBottomAnchor) == GUIAnchorStyles.Bottom)
165             {
166                 y = currentNode.y;
167             }
168             else if ((currentNode.Anchor & topBottomAnchor) == GUIAnchorStyles.Top)
169             {
170                 int diff = parent.height - currentNode.parentLastHeight;
171                 y = currentNode.y + diff;
172             }
173             else if ((currentNode.Anchor & topBottomAnchor) == topBottomAnchor)
174             {
175                 y = currentNode.y;
176             }
177             else
178             { throw new Exception(string.Format("Not expected Anchor:[{0}]!", currentNode.Anchor)); }
179 
180             currentNode.x = x; currentNode.y = y;
181             currentNode.width = width; currentNode.height = height;
182         }
183     }}
View Code

為了降低對.net庫的依賴,我根據.net自帶的Size、Point、Anchor等基礎的數據結構,復制了GUISize、GUIPoint、GUIAnchor……

渲染

用OpenGL渲染控件,實際上是如何在固定位置以固定大小畫圖的問題。

在OpenGL的渲染流水線上,描述頂點位置的坐標,依次要經過object space, world space, view/camera space, clip space, normalized device space, Screen/window space這幾個狀態。下表列出了各個狀態的特點。

+BIT祝威+悄悄在此留下版了個權的信息說:

Space

Coordinate

feature

object

(x, y, z, 1)

從模型中讀取的原始位置(x,y,z),可在shader中編輯

world

(x, y, z, w)

可在shader中編輯

view/camera

(x, y, z, w)

可在shader中編輯

clip

(x, y, z, w)

vertex shader中,賦給gl_Position的值

normalized device

(x, y, z, 1)

上一步的(x, y, z, w)同時除以w。OpenGL自動完成。x, y, z的絕對值小于1時,此頂點在窗口可見范圍內。即可見范圍為[-1, -1, -1]到[1, 1, 1]

screen/window

glViewport(x, y, width, height);

glDepthRange(near, far)

OpenGL以窗口左下角為(0, 0)。

上一步的頂點為(-1, -1, z)時,screen上的頂點為(x, y)。

上一步的頂點為(1, 1, z)時,screen上的頂點為(x + width, y + height)。

根據上表來看,object space, world space, view space三步可以省略跳過,而normalized device space是無法跳過的,所以我們在shader中給控件指定的坐標,就應該在[-1,-1,-1]和[1,1,1]之間。然后通過glViewport(x, y, width, height);指定控件的位置(x, y)和大小(width, height)。

為了避免影響到控件范圍外的東西,要啟用GL_SCISSOR_TEST

 1 using System;
 2 
 3 namespace CSharpGL
 4 {
 5     public abstract partial class GLControl
 6     {
 7         public virtual void RenderGUIBeforeChildren(GUIRenderEventArgs arg)
 8         {
 9             GL.Instance.Enable(GL.GL_SCISSOR_TEST);
10             GL.Instance.Scissor(this.absLeft, this.absBottom, this.width, this.height);
11             GL.Instance.Viewport(this.absLeft, this.absBottom, this.width, this.height);
12 
13             if (this.RenderBackground)
14             {
15                 vec4 color = this.BackgroundColor;
16                 GL.Instance.ClearColor(color.x, color.y, color.z, color.w);
17                 GL.Instance.Clear(GL.GL_COLOR_BUFFER_BIT);
18             }
19         }
20     }
21 }
+BIT祝威+悄悄在此留下版了個權的信息說:

事件

事件這個東西太復雜,我們來一點一點的說清楚其設計思路。

WinGLCanvas是一個WinForm控件,所有的OpenGL渲染的內容都在此顯示。

當我的WinForm控件WinGLCanvas收到一個消息(以鼠標按下mouse down為例)時,他會遍歷所有的GLControl,告訴他們“有mouse down消息來了”。每個控件都會調用自己關聯的mouseDown事件(如果有的話)。

然而細想一下,只有鼠標所在位置的那個GLControl才應該響應mouse Down消息。所以,在WinGLCanvas遍歷GLControl時,要分辨出哪個控件在mouse Down的位置,然后通知它;不通知其他控件。類似的,只有得到Focus的控件才會收到key down消息,從而調用自己的KeyDown事件。

繪制文字

做一個CtrlLabel,核心工作就是要在指定的位置繪制文字。

大致思路是這樣的:

首先,做出這樣的文字貼圖。當要繪制的文字比較多的時候,就會出現不止一張貼圖。這里為了便于演示,我故意把貼圖尺寸設定得比較小,從而出現了第二張貼圖;并且用金色邊框把貼圖的邊沿描繪出來,用紅色或綠色邊框把各個Glyph的位置和大小都表示出來。

 

 

然后,用OpenGL創建一個GL_TEXTURE_2D_ARRAY的紋理Texture,把上面這些貼圖都放進去。

最后,用一個Dictionary<char, GlyphInfo>字典記錄每個字符的字形信息(在Texture中的位置、大小)。

思路就這三步,詳情直接看代碼比較好(GlyphMap)。

需要注意的是,之前規定了“控件的頂點范圍應該在[-1,-1,-1]和[1,1,1]之間”。所以在給CtrlLabel設定好各個字形的位置后,還要按比例縮放到[-1,-1,-1]和[1,1,1]中,并且調整CtrlLabel的Width屬性。這樣才能正常顯示文字。這其實就是WinForm里的Label的AutoSize=true。

 

總結

看代碼看代碼看代碼。這里只有思路。

 

+BIT祝威+悄悄在此留下版了個權的信息說:

文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()