CSharpGL(45)自制控件的思路
CSharpGL(45)自制控件的思路
本文介绍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等)。
隶属关系
WinForm里的控件们构成了一个树形关系,CSharpGL也是这样。有了这样的隶属关系,就可以以相对于Parent的位置来记录自己的位置。
而且,当我做好了CtrlLabel,就可以直接放到CtrlButton.Children里,于是CtrlButton上显示文字的功能就瞬间实现了(当然还要设置一下文字位置,但工作量已经可以忽略不计了)。
using System; namespace CSharpGL
{
/// <summary>
/// Control(widget) in OpenGL window.
/// </summary>
public abstract partial class GLControl
{
internal GLControl parent;
[Description("Parent control. This node inherits parent's layout properties.")]
public GLControl Parent
{
get { return this.parent; }
set
{
GLControl old = this.parent;
if (old != value)
{
this.parent = value; if (value == null) // parent != null
{
old.Children.Remove(this);
}
else // value != null && parent == null
{
value.Children.Add(this);
}
}
}
} [Description("Children Nodes. Inherits this node's IWorldSpace properties.")]
public GLControlChildren Children { get; private set; } public GLControl(GUIAnchorStyles anchor)
{
this.Children = new GLControlChildren(this); this.Anchor = anchor;
}
}
}
布局属性
首先要有Location和Size。然后,在Parent的Size改变时,自己要相应的改变Location和Size,那么就需要Anchor来指定“是不是维持与某一边的距离不变”。
如何计算自己更新后的Location和Size?这是个简单的算法问题。
using System; namespace CSharpGL
{
public partial class GLControl
{
/// <summary>
/// 获取或设置控件绑定到的容器的边缘并确定控件如何随其父级一起调整大小。
/// </summary>
public GUIAnchorStyles Anchor { get; set; } private int x;
private int y; /// <summary>
/// 相对于Parent左下角的位置(Left Down location)
/// </summary>
public GUIPoint Location
{
get { return new GUIPoint(x, y); }
set { this.x = value.X; this.y = value.Y; }
} public GUISize Size
{
get { return new GUISize(width, height); }
set { this.width = value.Width; this.height = value.Height; }
} private int width;
private int height; /// <summary>
/// 上次更新之后,parent的Width属性值。
/// </summary>
private int parentLastWidth;
/// <summary>
/// 上次更新之后,parent的Height属性值。
/// </summary>
private int parentLastHeight; /// <summary>
///
/// </summary>
protected int absLeft;
/// <summary>
///
/// </summary>
protected int absBottom; /// <summary>
/// Layout for this control.
/// </summary>
public virtual void UpdateAbsoluteLocation()
{
GLControl parent = this.Parent;
if (parent != null)
{
this.absLeft = parent.absLeft + this.x;
this.absBottom = parent.absBottom + this.y;
}
else
{
this.absLeft = this.x;
this.absBottom = this.y;
}
} /// <summary>
/// layout controls in OpenGL canvas.(
/// Updates absolute and relative (location and size) of specified node and its children nodes.
/// <para>This coordinate system is shown as below.</para>
/// <para> /\ y</para>
/// <para> |</para>
/// <para> |</para>
/// <para> |</para>
/// <para> |</para>
/// <para> |</para>
/// <para> |----------------->x</para>
/// <para>(0, 0)</para>
/// </summary>
/// <param name="node"></param>
public static void Layout(GLControl node)
{
if (node == null) { return; } var parent = node.Parent;
if (parent != null)
{
NonRootNodeLayout(node, parent);
} node.UpdateAbsoluteLocation(); foreach (var item in node.Children)
{
GLControl.Layout(item);
} if (parent != null)
{
node.parentLastWidth = parent.width;
node.parentLastHeight = parent.height;
}
} private const GUIAnchorStyles leftRightAnchor = (GUIAnchorStyles.Left | GUIAnchorStyles.Right);
private const GUIAnchorStyles topBottomAnchor = (GUIAnchorStyles.Top | GUIAnchorStyles.Bottom); /// <summary>
/// Updates <paramref name="currentNode"/>'s location and size according to its state and parent's information.
/// </summary>
/// <param name="currentNode"></param>
/// <param name="parent"></param>
private static void NonRootNodeLayout(GLControl currentNode, GLControl parent)
{
int x, y, width, height;
if ((currentNode.Anchor & leftRightAnchor) == leftRightAnchor)
{
width = parent.width - currentNode.parentLastWidth + currentNode.width;
if (width < ) { width = ; }
}
else
{
width = currentNode.width;
} if ((currentNode.Anchor & topBottomAnchor) == topBottomAnchor)
{
height = parent.height - currentNode.parentLastHeight + currentNode.height;
if (height < ) { height = ; }
}
else
{
height = currentNode.height;
} if ((currentNode.Anchor & leftRightAnchor) == GUIAnchorStyles.None)
{
int diff = parent.width - currentNode.parentLastWidth;
x = currentNode.x + diff / ;
}
else if ((currentNode.Anchor & leftRightAnchor) == GUIAnchorStyles.Left)
{
x = currentNode.x;
}
else if ((currentNode.Anchor & leftRightAnchor) == GUIAnchorStyles.Right)
{
int diff = parent.width - currentNode.parentLastWidth;
x = currentNode.x + diff;
}
else if ((currentNode.Anchor & leftRightAnchor) == leftRightAnchor)
{
x = currentNode.x;
}
else
{ throw new Exception(string.Format("Not expected Anchor:[{0}]!", currentNode.Anchor)); } if ((currentNode.Anchor & topBottomAnchor) == GUIAnchorStyles.None)
{
int diff = parent.height - currentNode.parentLastHeight;
y = currentNode.y + diff / ;
}
else if ((currentNode.Anchor & topBottomAnchor) == GUIAnchorStyles.Bottom)
{
y = currentNode.y;
}
else if ((currentNode.Anchor & topBottomAnchor) == GUIAnchorStyles.Top)
{
int diff = parent.height - currentNode.parentLastHeight;
y = currentNode.y + diff;
}
else if ((currentNode.Anchor & topBottomAnchor) == topBottomAnchor)
{
y = currentNode.y;
}
else
{ throw new Exception(string.Format("Not expected Anchor:[{0}]!", currentNode.Anchor)); } currentNode.x = x; currentNode.y = y;
currentNode.width = width; currentNode.height = height;
}
}}
为了降低对.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这几个状态。下表列出了各个状态的特点。
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。
using System; namespace CSharpGL
{
public abstract partial class GLControl
{
public virtual void RenderGUIBeforeChildren(GUIRenderEventArgs arg)
{
GL.Instance.Enable(GL.GL_SCISSOR_TEST);
GL.Instance.Scissor(this.absLeft, this.absBottom, this.width, this.height);
GL.Instance.Viewport(this.absLeft, this.absBottom, this.width, this.height); if (this.RenderBackground)
{
vec4 color = this.BackgroundColor;
GL.Instance.ClearColor(color.x, color.y, color.z, color.w);
GL.Instance.Clear(GL.GL_COLOR_BUFFER_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。
总结
看代码看代码看代码。这里只有思路。
CSharpGL(45)自制控件的思路的更多相关文章
- MyWidget【简单自制控件】
#coding=gbk from PyQt4 import QtGui,QtCore import random class MyWidget(QtGui.QWidget): def __init__ ...
- app每个页面都有一个相同的浮层控件 实现思路
可以创建一个window,设置其windowLevel为alert;
- CSharpGL(26)在opengl中实现控件布局/渲染文字
CSharpGL(26)在opengl中实现控件布局/渲染文字 效果图 如图所示,可以将文字.坐标轴固定在窗口的一角. 下载 CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入( ...
- 微软BI SSIS 2012 ETL 控件与案例精讲课程学习方式与面试准备详解
开篇介绍 微软BI SSIS 2012 ETL 控件与案例精讲 (http://www.hellobi.com/course/21) 课程从2014年9月开始准备,到2014年12月在 天善BI学院 ...
- UWP学习记录4-设计和UI之控件和模式1
UWP学习记录4-设计和UI之控件和模式1 1.控件和事件简介 在 UWP 应用开发中,控件是一种显示内容或支持交互的 UI 元素. 控件是用户界面的构建基块. 我们提供了超过 45 种控件供你使用, ...
- 自定义angularjs分页控件
继昨天写了knockoutjs+ jquery pagination+asp.net web Api 实现无刷新列表页 ,正好最近刚学习angularjs ,故琢磨着写一个angularjs版本的分页 ...
- 天津政府应急系统之GIS一张图(arcgis api for flex)讲解(四)地图导航控件模块
config.xml文件的配置如下: <widget left="10" top="50" config="widgets/Navigation ...
- C# 自定义FileUpload控件
摘要:ASP.NET自带的FileUpload控件会随着浏览器的不同,显示的样式也会发生改变,很不美观,为了提高用户体验度,所以我们会去自定义FileUpload控件 实现思路:用两个Button和T ...
- Android 在布局容器中动态添加控件
这里,通过一个小demo,就可以掌握在布局容器中动态添加控件,以动态添加Button控件为例,添加其他控件同样道理. 1.addView 添加控件到布局容器 2.removeView 在布局容器中删掉 ...
随机推荐
- 页面设计-数据列表 DataGrid
传统软件项目开发时,针对每个业务单据页面需要每控件一个一个的来设计,同时需要在页面功能中对每个控件的属性进行判定处理,尤其是页面风格布局样式需要花去一大半的时间,并且后续要想修改是非常麻烦繁琐,这样就 ...
- FoxOne---一个快速高效的BS框架--数据访问(Dao)
FoxOne---一个快速高效的BS框架--(1) FoxOne---一个快速高效的BS框架--(2) FoxOne---一个快速高效的BS框架--(3) FoxOne---一个快速高效的BS框架-- ...
- PHP中public、protected、private权限修饰符
PHP中有三种访问修饰符 默认是public public(公共的.默认) protected(受保护的) private(私有的) 访问权限 public protected private 类内 ...
- redis C接口hiredis 简单函数使用介绍
hiredis是redis数据库的C接口,目前只能在linux下使用,几个基本的函数就可以操作redis数据库了. 函数原型:redisContext *redisConnect(const char ...
- win 7 系统ie浏览器升级11版本后,f12功能不可用的问题
自从把ie8升级成11后,f12功能就不可用了.浏览器兼容模式也无法使用. 解决办法:下载windows补丁 IE11-Windows6.1-KB3008923-x64.msu 下载地址: 64位:h ...
- bootstrap 轮播模板
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 一道javascript面试题(闭包与函数柯里化)
要求写一个函数add(),分别实现能如下效果: (1)console.log(add(1)(2)(3)(4)()); (2)console.log(add(1,2)(3,4)()); (3)conso ...
- git学习整理(1)git clone 理解
1.git clone 的理解 git clone默认会把远程仓库整个给clone下来 ,只能clone远程库的master分支并在本地默认创建一个master分支 ,无法clone所有分支,若想要其 ...
- Extjs6(五)——写一个包含toolbar、form、grid的子页面
本文基于ext-6.0.0 这个页面布局是很多管理系统的常用布局,具体如下图: 一.页面主体personalInfo.js 整个页面采用border布局,分成三部分,这个personalInfo.js ...
- win10 uwp 简单MasterDetail
中文 English 本文主要讲实现一个简单的界面,可以在窗口比较大显示列表和内容,窗口比较小时候显示列表或内容.也就是在窗口比较小的时候,点击列表会显示内容,点击返回会显示列表. 先放图,很简单. ...