UltraDynamo (第四部分) - 图形和字体
在本节中,我们将介绍一些图形和字体处理例程。
目录
注意:本文是多部分文章。下载内容可在第一部分中找到。
引言
在应用程序中,有各种图形(图像)用于向用户呈现信息。例如,有
- 指南针玫瑰
- 汽车侧视图
- 汽车前视图
- 汽车俯视图
这些图像作为资源存储在应用程序中。在 UltraDynamo 的早期版本中,用户只能使用这些图像,但稍后,我可能会允许用户选择自己的图像用于应用程序。
这些图像是 jpg 文件,我将它们存储在项目解决方案的“/images”文件夹中。将图像复制到解决方案后,为了使它们作为嵌入式资源可供代码使用,而不是作为随主可执行文件一起出现的附加文件,需要为每个文件设置几个属性。如果我们查看指南针玫瑰的属性
您会注意到 **生成操作** 已设置为 **嵌入式资源**,并且 **复制到输出目录** 已设置为 **不复制**。
加载资源
接下来我们需要能够在运行时将图像加载到应用程序中并向用户显示。为了实现这一点,我创建了一个帮助函数,该函数向调用者返回一个包含图像资源的流。执行此操作的函数如下所示
private Stream LoadCompassImage()
{
Assembly _assembly;
Stream _imageStream;
_assembly = Assembly.GetExecutingAssembly();
_imageStream = _assembly.GetManifestResourceStream("UltraDynamo.Images.compass_200x200.jpg");
return _imageStream;
}
正如您在代码中看到的,我们首先获取对正在运行的程序集的引用,然后将内部图像的路径设置为 string
。
旋转图像
除非指南针根据传感器当前航向进行旋转,否则它几乎没有用处。指南针通常会将用户的航向保持在顶部,并相应地旋转指南针。旋转函数如下所示,我们只需传入要旋转的图像和旋转角度。该函数返回旋转后的图像。
private Image getRotatedImage(Image image,float angle)
{
//Create a new image based on the original
Bitmap rotated = new Bitmap(image);
//Create a graphics object to work with the image
Graphics g = Graphics.FromImage(rotated);
//Move the rotation point to centre by moving image
g.TranslateTransform((float)image.Width / 2, (float)image.Height / 2);
//rotate
g.RotateTransform(angle);
//undo the image for rotation point
g.TranslateTransform(-(float)image.Width / 2, -(float)image.Height / 2);
//draw source image on graphics object
g.DrawImage(image, new Point(0, 0));
//Return the rotated image
return rotated;
}
一个重要的事实是,图像通常围绕其左上角进行旋转。我们需要能够围绕图像中心进行旋转。这需要进行变换,将图像移动到正确的旋转点,然后在旋转完成后将其移回其原始位置。
实际上,指南针航向需要是一个负数,否则指南针会朝错误的方向旋转,因此我们只需将传感器提供的航向乘以 -1
,然后再将其传递给旋转函数。
我使用了一个用户控件来托管指南针。这样就可以轻松地将控件放置在应用程序中的任何需要的位置。paint 事件处理程序是我们放置代码以实际将图像绘制到用户控件表面的地方。
下面的代码放在 paint
事件中,您可以看到图像正在被加载,指南针航向正在被读取并乘以 -1
以获得正确的旋转方向,然后进行旋转
Graphics g = e.Graphics;
//Load and rotate the image
g.DrawImage(getRotatedImage(Image.FromStream(LoadCompassImage()),
(float)compassValues.Heading *-1), 0, 0, this.Width, this.Height);
我做的最后一件事是在指南针顶部绘制一条线作为航向指示器。画笔被定义为控件的 private
成员,因此不需要在每个 paint
事件中定义它,然后使用 pen
简单地绘制线条
//Heading Line (Approx 75% opacity Red Pen)
Pen headingPen = new Pen(Color.FromArgb(190, 255, 0, 0), 3);
//Draw Heading Line NOTE: the -1 is to take into account the pen thickness of 3 to ensure line central
g.DrawLine(headingPen, new Point((this.Width / 2) -1, this.Height / 2),
new Point((this.Width/2)-1, (this.Height - (int)(this.Height * 0.92))));
下面,您可以看到加载到独立窗体窗口中的旋转指南针玫瑰和航向线是什么样子的;
倾斜仪视图
三个汽车图像用于显示车辆的横滚、俯仰和偏航。这些图像的处理方式与指南针玫瑰非常相似。
然而,这些视图的主要区别在于增加了角度显示以及基线和偏移角度线的参考线。
倾斜仪视图也采用了比指南针视图稍有改进/优化(我将在未来努力更新),即基本图像仅在用户控件首次实例化时从程序集中加载。它不会在每次 paint 事件时重复加载。图像仅存储为 private
成员变量,并在用户控件构造时加载图像。
三角学
在绘制线条时,最难的部分是计算旋转线条的起点和终点在相对于点的 X/Y 坐标中的位置。而这正是“多年未使用或需要的”三角学发挥作用的地方。是的,**Sohcahtoa** 又来困扰我了!
我不知道你们其他人怎么样,但对我来说,自从在学校学过它,然后在学徒期间可能再次复习过它之后,我再也没有用过它。对我来说,我需要去 Google 搜索一些复习资料。您可以在 [此处] 参考其中一个页面。
基本上,我们知道旋转角度,并且知道我们正在绘制的显示表面的宽度(和高度)。使用这些作为三角函数的参数,我们可以计算其他线长,从而得出点的 (x,y) 坐标。
如果我们看倾斜仪的俯仰角,我们首先绘制水平参考线 (Pens.Blue
),然后使用三角学计算旋转线 (Pens.Red
) 的 (x,y) 坐标。
//Pitch or Roll - Horizontal
g.DrawLine(Pens.Blue, new Point(0, this.Height / 2), new Point(this.Width, this.Height / 2));
if (this.InclinometerViewState == InclinometerViewOptions.Pitch)
{
try
{
g.DrawLine(Pens.Red,
new Point(0,(int)((this.Height / 2) - ((this.Width / 2) *
Math.Tan(((double)inclinometerValues.Pitch * Math.PI) / 180)))),
new Point(this.Width,(int)((this.Height / 2) + ((this.Width / 2) *
Math.Tan(((double)inclinometerValues.Pitch * Math.PI) / 180)))));
}
catch (Exception)
{
//TODO: Pitch Overflow improvement
//This is just a dirty catch for a Pitch of 90'
//which causes an overflow exception. - Need to find improved approach later
g.DrawLine(Pens.Red, new Point(this.Width/2, 0), new Point(this.Width/2, this.Height));
}
}
您还会注意到那里有一个“脏代码”(dirty hack)来捕获当角度达到 90 度时发生的溢出,我们只需绘制一条我们知道它应该在的直线。这是我将来需要更详细研究的另一个领域。
绘制角度字符串
我还将在倾斜仪视图的顶部中心写出角度。我们必须计算此 string
的大小,以便计算 string
将在窗体上的位置。
if (this.ShowAngleValue)
{
Font f = this.Font;
f = new Font(f.FontFamily, 18, FontStyle.Bold);
SizeF testSize = g.MeasureString(angle, f);
g.DrawString(angle, f, Brushes.Green, new Point((this.Width - (int)testSize.Width) / 2, 0));
}
您会注意到这一点被放置在一个 if{}
语句中,我添加了一些 private
变量,以便将来可以将应用程序更改为用户对显示器有更多的配置选项。目前,这些都是固定的。
随着图像旋转、角度文本和参考线都添加到倾斜仪视图中,下面的图像显示了它们的样子
使字体适应空间
在开发指南针航向显示时,字体的一个有趣方面浮现出来。指南针航向显示仅向用户显示 N、NE、E、SE、S、SW、W、NW 中的一个,以提供方向的总体概念。
事实证明,您不能简单地将一个给定的矩形提供给图形对象,然后说“用允许的最大文本填充这个矩形,根据 string
和矩形大小”,而是必须物理地迭代字体,增加它们,直到输出大小超过可用空间,然后减小一个尺寸。
如果您看一下下面的代码,您可以看到这个过程是如何工作的
private void UICompassHeadingLetters_Paint(object sender, PaintEventArgs e)
{
String output = "N"; //Default to North removes need for two ifs below
//Get the string representation of the heading
double heading = compassValues.Heading;
if (heading >= 22.5 && heading < 67.5)
output = "NE";
if (heading >= 67.5 && heading < 112.5)
output = "E";
if (heading >= 112.5 && heading < 157.5)
output = "SE";
if (heading >= 157.5 && heading < 202.5)
output = "S";
if (heading >= 202.5 && heading < 247.5)
output = "SW";
if (heading >= 247.5 && heading < 292.5)
output = "W";
if (heading >= 292.5 && heading < 337.5)
output = "NW";
//Establish base fonts and graphics objects
Graphics g = e.Graphics;
Font f = this.Font;
Font newFont = this.Font;
SizeF testSize;
float fHeight=0;
float fWidth=0;
bool fontGood = true;
for (int newSize = 1; fontGood; newSize++)
{
//Increase the size and test height again)
newFont=new Font(f.Name,newSize,f.Style);
testSize = g.MeasureString(output,newFont);
//Determine boundary size for the text
fHeight = testSize.Height;
fWidth = testSize.Width;
//Test the text height and width against control size
if ((fHeight > (this.Height)) | (fWidth > (this.Width)))
{
fontGood = false;
newSize--; //go back by 1 size,
//recalculate size required for positioning central on control
if (newSize <6 ) { newSize=6;};
newFont = new Font(f.Name,newSize,f.Style);
testSize = g.MeasureString(output,newFont);
fHeight = testSize.Height;
fWidth = testSize.Width;
}
}
//Draw the text heading on the control
g.DrawString(output, newFont, Brushes.Green,
new Point((this.Width - (int)fWidth) / 2, (this.Height - (int)fHeight) / 2));
首先,我们读取指南针传感器的航向值,并将其转换为相关的 string
表示形式,例如“NW
”。然后我们获取 grabs
对象,选择一个小字体并计算它占用的空间大小。如果它小于可用空间,我们就尝试下一个尺寸。如果它超过可用空间,我们就知道前一个尺寸是正确的最大尺寸,因此使用该尺寸再次绘制它并将其绘制到图形表面。
一定有更有效的方法来做这件事,但这是以后的事了!您可以看到指南针航向窗体会随着窗体大小的调整自动缩放文本以适应绘图表面
双缓冲
这可能是您绝对不能忘记启用的最重要的一件事。在第一个版本中我忘记了,结果导致显示器“闪烁”。所以,无论是 Windows 窗体还是 UserControl
,都不要忘记将 DoubleBuffered
属性设置为 True
。
您可以在 这里 阅读它确切的作用。
挂起您的布局
您还应该做的另一件事是在 paint
事件期间挂起布局。这基本上可以防止大量屏幕闪烁等问题,并且所有更改的各个方面都会被绘制到窗体或控件的表面上。您在 paint
事件的开头挂起布局,然后在离开 paint
事件之前立即恢复它们,例如
private void UICompassFull_Paint(object sender, PaintEventArgs e)
{
//Suspend
this.SuspendLayout();
//Do all your painting here
//Resume
this.ResumeLayout();
}
您可以在 这里 阅读更多关于此的信息。
进入下一部分
第五部分将介绍部署的构建、打包和代码签名方面。