如何建个人网站教程南通科技网站建设
如何建个人网站教程,南通科技网站建设,东莞阳光网投诉中心,上海网站建设多少1. 为什么我们需要纵向文字标签#xff1f;
你有没有遇到过这样的场景#xff1f;在用C# Winform做桌面应用的时候#xff0c;界面上需要展示一些竖排的文字#xff0c;比如一个仿古风格的软件标题、一个侧边栏的垂直导航菜单#xff0c;或者是一个仪表盘上竖直显示的数据…1. 为什么我们需要纵向文字标签你有没有遇到过这样的场景在用C# Winform做桌面应用的时候界面上需要展示一些竖排的文字比如一个仿古风格的软件标题、一个侧边栏的垂直导航菜单或者是一个仪表盘上竖直显示的数据指标。这时候你兴冲冲地拖一个Label控件到窗体上然后傻眼了——Label控件它天生就是横着排版的根本没有提供任何属性让你把文字竖过来。你可能会想这还不简单网上搜一下呗。结果一搜大部分教程都告诉你用System.Drawing.Graphics的DrawString方法设置一个StringFormatFlags.DirectionVertical标志就能画出来。你照着做了代码一跑嘿文字果然竖着显示出来了心里正美呢问题马上就来了当你需要更新这个标签的文字时比如从“未连接”变成“已连接”你直接再调用一次绘制方法会发现新旧文字重叠在了一起界面上一片狼藉。这就是网上很多“快餐式”教程没告诉你的坑。它们只教你怎么“画”上去却没教你怎么“擦”干净。你可能会灵机一动“那我用背景色在原位置再画一遍不就把旧的覆盖掉了吗”这个想法很自然我也这么试过。但实测下来在Windows系统下因为字体渲染有**平滑边缘抗锯齿**效果文字的边缘会有半透明的像素。你用纯色背景去覆盖这些半透明的“影子”根本擦不干净会留下淡淡的痕迹看起来非常不专业界面显得很“脏”。所以我们今天要聊的不仅仅是怎么把文字竖着画出来更要解决这个“画上去就擦不掉”的核心痛点。我会带你从零开始手把手实现一个真正可用、可维护、显示效果干净的纵向文字标签控件。这个控件要能做到想显示什么就显示什么想改就改改完之后界面清清爽爽就像系统自带的Label控件一样可靠。2. 核心原理Graphics绘图与“画板”管理要彻底理解怎么解决文字重叠和擦除不净的问题我们得先搞明白Winform绘图的底层机制。你可以把整个窗体Form想象成一块画布而System.Drawing.Graphics对象就是你手里的画笔。我们用Graphics的各种方法如DrawString在画布上作画。关键点来了这块“画布”是公用的。你的按钮、文本框、还有我们自定义画的竖排文字最终都显示在这同一块画布上。当你调用DrawString画文字时它只是在当前帧的画布像素上叠加了新的颜色信息并不会“记住”自己画了什么。所以当你要更新文字时如果没有一个明确的“擦除”指令新画上去的像素就会和旧的像素混合在一起造成重叠。那么正确的“擦除”姿势是什么并不是用背景色去覆盖而是使用Graphics.Clear(Color)方法。这个方法的作用是用指定的颜色清空整个Graphics对象所关联的绘图表面。听起来很强大但直接用在窗体上会出大问题this.CreateGraphics()得到的Graphics对象关联的是整个窗体画布你一Clear好家伙整个窗体包括其他所有控件都被清成一片纯色了这显然不行。所以我们解决方案的核心思路是开辟一块“专属画板”。我们不直接在窗体这块大画布上作画而是为我们的纵向标签创建一块独立、固定大小的“小画板”比如一个Bitmap对象。所有的绘制和擦除操作都只发生在这块小画板上。画好之后再把整块小画板的内容“贴”到窗体大画布的指定位置。这样每次更新文字时我们只需要清空自己的小画板完全不会影响到窗体的其他部分。这就像你在墙上贴便签纸。直接在墙上写字很难修改。但你先在一张小纸片Bitmap上写好字然后用胶带DrawImage把小纸片贴在墙上。想换内容时只需换一张写有新内容的小纸片或者把旧纸片撕下来对应从窗体上擦除旧图再贴新的。墙面窗体本身始终保持干净。下面我们就用代码来实现这个“专属画板”的思路。// 首先我们定义一个纵向标签类 public class VerticalLabel { // 这是我们的“小画板” private Bitmap _labelBitmap; // 这是在小画板上作画的“画笔” private Graphics _bitmapGraphics; // 标签的宽度和高度注意竖排文字宽度是字符高度高度是字符串总宽度 private int _width, _height; // 标签在窗体上的位置 private Point _location; // 我们需要知道把画板“贴”到哪个窗体上 private Control _parentControl; // 构造函数初始化我们的专属画板 public VerticalLabel(Control parent, int width, int height, Point location) { _parentControl parent; _width width; _height height; _location location; // 创建一块指定大小的空白画板Bitmap _labelBitmap new Bitmap(width, height); // 获取在这块画板上绘图的Graphics对象 _bitmapGraphics Graphics.FromImage(_labelBitmap); // 设置高质量绘图参数减少锯齿 _bitmapGraphics.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; _bitmapGraphics.TextRenderingHint System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; } // 核心方法在画板上绘制竖排文字 public void DrawText(string text, Font font, Brush brush) { // 第一步清空画板。这里用透明色清空为后续透明背景做准备。 _bitmapGraphics.Clear(Color.Transparent); // 第二步设置竖排格式 StringFormat format new StringFormat(); format.FormatFlags StringFormatFlags.DirectionVertical; // 第三步在画板0,0位置开始绘制文字 // 注意因为画板大小就是我们为标签预留的大小所以从原点开始画即可。 _bitmapGraphics.DrawString(text, font, brush, 0, 0, format); // 第四步触发一次重绘将画板内容贴到窗体上 RefreshParent(); } // 将画板内容绘制到父控件上 private void RefreshParent() { // 获取父控件窗体的Graphics对象 using (Graphics parentGraphics _parentControl.CreateGraphics()) { // 将我们画好的_bitmap一次性绘制到父控件的指定位置 parentGraphics.DrawImage(_labelBitmap, _location); } } // 清理资源非常重要 public void Dispose() { _bitmapGraphics?.Dispose(); _labelBitmap?.Dispose(); } }我来解释一下这段代码的几个关键优化点使用Bitmap作为缓冲_labelBitmap就是我们的专属小画板。所有绘制先在这里完成。透明清空_bitmapGraphics.Clear(Color.Transparent)用透明色清空画板。这意味着画板未被文字覆盖的区域是完全透明的当贴到窗体上时不会遮挡后面的控件或背景。高质量渲染设置我们设置了SmoothingMode和TextRenderingHint这让竖排文字的边缘更加平滑清晰尤其是在小字体下显示效果提升非常明显。集中绘制DrawText方法集成了清空和绘制并自动刷新显示。对外只需要一个方法调用简化了使用流程。3. 从基础实现到健壮控件解决闪烁与刷新按照上面的代码我们已经实现了一个不重叠、可擦除的纵向标签。但是如果你把它放到一个会频繁刷新或者有动画的界面里可能会发现标签区域有时会闪烁。这是因为我们直接在窗体的Graphics上调用DrawImage这是一种即时模式Immediate Mode绘图没有和Winform的窗口消息循环同步。Winform控件标准的绘图方式是通过处理Paint事件在事件参数PaintEventArgs提供的Graphics上进行绘制。这种方式由系统统一管理能有效避免闪烁。所以为了让我们的纵向标签更健壮、更像一个标准控件我们需要升级方案将标签画板的内容绘制到控件的Paint事件中。同时我们也不希望每次画文字都手动new Font和new Brush最好能像普通Label一样设置好Font、ForeColor等属性自动生效。我们来创建一个继承自Control的自定义控件这样它就能拥有所有标准控件的特性如设计时支持、数据绑定、事件等。// 创建一个自定义控件继承自Control public class VerticalLabelControl : Control { // 私有字段 private Bitmap _bufferBitmap; // 缓冲位图 private string _displayText string.Empty; // 公开属性 public string VerticalText { get { return _displayText; } set { if (_displayText ! value) { _displayText value; // 文本改变重新绘制缓冲并刷新 RenderToBuffer(); this.Invalidate(); // 请求重绘触发Paint事件 } } } // 构造函数 public VerticalLabelControl() { // 设置控件默认样式减少闪烁 this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.ResizeRedraw | ControlStyles.OptimizedDoubleBuffer, true); this.UpdateStyles(); // 默认大小竖排窄而高 this.Size new Size(20, 100); } // 当控件大小改变时需要重新创建合适大小的缓冲位图 protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); // 释放旧的位图 _bufferBitmap?.Dispose(); // 创建新的、与控件客户区同样大小的缓冲位图 _bufferBitmap new Bitmap(this.ClientSize.Width, this.ClientSize.Height); // 重新渲染文本到新缓冲 RenderToBuffer(); } // 核心方法将文本渲染到缓冲位图 private void RenderToBuffer() { if (_bufferBitmap null || string.IsNullOrEmpty(_displayText)) return; using (Graphics g Graphics.FromImage(_bufferBitmap)) { // 用透明色清空缓冲 g.Clear(Color.Transparent); // 设置高质量绘图 g.SmoothingMode System.Drawing.Drawing2D.SmoothingMode.AntiAlias; g.TextRenderingHint System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; // 创建竖排格式 StringFormat format new StringFormat(); format.FormatFlags StringFormatFlags.DirectionVertical; // 你可以根据需要调整对齐方式比如垂直居中 format.Alignment StringAlignment.Center; format.LineAlignment StringAlignment.Near; // 使用控件的Font和ForeColor属性 using (Brush textBrush new SolidBrush(this.ForeColor)) { // 在缓冲位图的整个矩形区域内绘制文本 Rectangle textRect new Rectangle(0, 0, _bufferBitmap.Width, _bufferBitmap.Height); g.DrawString(_displayText, this.Font, textBrush, textRect, format); } } } // 重写OnPaint在这里将缓冲位图绘制到屏幕上 protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); if (_bufferBitmap ! null) { // 将预先渲染好的缓冲位图一次性绘制到控件表面 e.Graphics.DrawImage(_bufferBitmap, Point.Empty); } // 如果需要也可以在这里直接绘制边框等 // e.Graphics.DrawRectangle(Pens.Gray, 0, 0, this.Width - 1, this.Height - 1); } // 清理资源 protected override void Dispose(bool disposing) { if (disposing) { _bufferBitmap?.Dispose(); } base.Dispose(disposing); } }这个版本是一个质的飞跃。我们通过几个关键步骤解决了闪烁和集成度问题继承ControlVerticalLabelControl现在是一个真正的Winform控件你可以把它拖到工具箱在设计器里设置Font、ForeColor、BackColor虽然背景我们用透明但属性存在等。双缓冲与样式设置SetStyle中设置OptimizedDoubleBuffer等标志这是Winform内置的双缓冲机制能极大减少绘图时的闪烁。缓冲位图Buffer Bitmap我们不在OnPaint事件中直接进行复杂的文本绘制和格式计算而是提前在RenderToBuffer方法中将最终效果画到_bufferBitmap里。OnPaint中只做一件事将这张准备好的位图DrawImage到屏幕上。这是一次非常快的操作极大地提升了绘制效率消除了闪烁感。属性驱动通过VerticalText属性来改变文本。属性设置器中调用Invalidate()这是标准控件的做法通知系统“我这个控件的外观需要更新了”系统会在合适的时机触发OnPaint。这样就把我们的绘制行为完美融入到了Winform的消息循环中。4. 高级优化性能、对齐与抗锯齿陷阱基础功能稳定后我们可以追求更极致的体验。这里有几个我实际项目中踩过的坑和优化点。4.1 性能优化避免频繁创建和销毁对象注意看之前代码的RenderToBuffer方法它在每次重绘时都会new StringFormat和new SolidBrush。如果文本更新非常频繁比如用于显示实时变化的传感器读数这会产生大量的小对象增加垃圾回收GC的压力。一个简单的优化是将这些不变的对象提升为控件的成员变量并只在需要时重新创建。public class VerticalLabelControl : Control { // ... 其他字段 ... private StringFormat _verticalStringFormat; private Brush _textBrush; public VerticalLabelControl() { // ... 初始化 ... _verticalStringFormat new StringFormat(); _verticalStringFormat.FormatFlags StringFormatFlags.DirectionVertical; _verticalStringFormat.Alignment StringAlignment.Center; _verticalStringFormat.LineAlignment StringAlignment.Near; // 初始文本刷子 UpdateTextBrush(); } // 当ForeColor属性改变时更新文本刷子 protected override void OnForeColorChanged(EventArgs e) { base.OnForeColorChanged(e); UpdateTextBrush(); RenderToBuffer(); this.Invalidate(); } private void UpdateTextBrush() { _textBrush?.Dispose(); // 释放旧的刷子 _textBrush new SolidBrush(this.ForeColor); } private void RenderToBuffer() { // ... 清空缓冲等操作 ... // 直接使用成员变量_format和_brush避免重复创建 g.DrawString(_displayText, this.Font, _textBrush, textRect, _verticalStringFormat); } protected override void Dispose(bool disposing) { if (disposing) { _bufferBitmap?.Dispose(); _verticalStringFormat?.Dispose(); // 记得释放StringFormat _textBrush?.Dispose(); } base.Dispose(disposing); } }4.2 精准对齐让文字居中显示竖排文字的对齐也是个麻烦事。DrawString默认是从左上角开始绘制。如果我们希望文字在控件的水平方向视觉上的垂直方向居中显示就需要计算。StringFormat.Alignment属性可以控制矩形区域内文本的水平对齐对竖排文字来说就是垂直方向的对齐。但有时候你会发现即使用了StringAlignment.Center文字看起来还是有点偏。这是因为字体有内边距Padding和行距Leading。一个更精准的办法是使用Graphics.MeasureString方法先测量出文本渲染后实际占据的矩形区域然后根据这个区域大小来调整绘制起点。private void RenderToBufferWithPreciseAlignment() { // ... 初始化Graphics g ... g.Clear(this.BackColor); // 这次我们用控件的BackColor方便看效果 StringFormat format new StringFormat(StringFormatFlags.DirectionVertical); format.Alignment StringAlignment.Center; format.LineAlignment StringAlignment.Near; format.Trimming StringTrimming.EllipsisCharacter; // 文本太长时显示省略号 Rectangle layoutRect new Rectangle(0, 0, this.Width, this.Height); // 关键步骤测量文本 SizeF textSize g.MeasureString(_displayText, this.Font, layoutRect.Size, format); // textSize.Width 是文本占用的“高度”因为竖排textSize.Height 是文本占用的“宽度” // 计算居中偏移量 float xOffset (this.Width - textSize.Height) / 2; // 水平居中偏移视觉上的垂直居中 float yOffset 0; // 从顶部开始 // 创建一个偏移后的矩形进行绘制 RectangleF drawRect new RectangleF(xOffset, yOffset, textSize.Height, textSize.Width); g.DrawString(_displayText, this.Font, _textBrush, drawRect, format); }通过测量和手动计算偏移我们可以实现像素级的精准对齐这对于要求严格的UI设计非常重要。4.3 抗锯齿的“陷阱”与ClearType我们之前设置了TextRenderingHint ClearTypeGridFit来获得清晰的字体。ClearType是Windows利用液晶像素排列特性优化字体显示的绝技在大多数情况下效果拔群。但是它有一个前提要求背景是纯色不透明的。因为ClearType会利用背景色来调整子像素如果背景是透明或者渐变的ClearType的效果会大打折扣甚至可能让文字边缘出现彩边。所以这里有一个重要的取舍场景一标签背景固定为纯色如白色、灰色。强烈推荐使用ClearTypeGridFit文字锐利清晰。场景二标签背景透明或复杂如放在一张图片上。这时应该使用AntiAliasGridFit。它使用标准的灰度抗锯齿不依赖背景色在复杂背景下表现更稳定但锐利度稍逊于ClearType。你可以在控件的属性中增加一个选项让使用者根据实际情况选择。public System.Drawing.Text.TextRenderingHint TextRenderingHint { get; set; } System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; private void RenderToBuffer() { using (Graphics g Graphics.FromImage(_bufferBitmap)) { g.TextRenderingHint this.TextRenderingHint; // 使用用户设置的渲染提示 // ... 其余绘制代码 ... } }5. 实战封装成可复用的用户控件库经过以上优化我们的VerticalLabelControl已经非常强大了。但如果我们项目里到处都要用每次都复制粘贴代码太麻烦。最好的办法是把它封装成一个独立的类库DLL或者至少是一个用户控件库这样可以在不同项目中轻松复用。新建项目在Visual Studio中新建一个“Windows 窗体控件库(.NET Framework)”项目命名为MyCompany.Controls。添加控件删除默认的UserControl1.cs添加一个新的类将我们最终版的VerticalLabelControl代码粘贴进去。添加设计时特性为了让控件在设计时工具箱和属性窗口有更好的体验我们可以添加一些特性。// 在类定义上方添加 [ToolboxBitmap(typeof(Label))] // 在工具箱中显示Label的图标 [Description(显示纵向文本的标签控件)] public class VerticalLabelControl : Control { // ... 控件的所有代码 ... // 为VerticalText属性添加设计时描述和分类 [Category(Appearance)] // 在属性窗口中归到“外观”分类 [Description(获取或设置控件显示的纵向文本)] public string VerticalText { get { return _displayText; } set { if (_displayText ! value) { _displayText value; RenderToBuffer(); this.Invalidate(); // 可选触发一个自定义事件通知外部文本已更改 OnVerticalTextChanged(EventArgs.Empty); } } } // 定义一个自定义事件 public event EventHandler VerticalTextChanged; protected virtual void OnVerticalTextChanged(EventArgs e) { VerticalTextChanged?.Invoke(this, e); } }生成与使用编译这个控件库项目会生成一个MyCompany.Controls.dll文件。在其他Winform项目中右键“引用”-“添加引用”浏览到这个DLL文件。然后在工具箱中右键“选择项...”点击“浏览”同样找到这个DLL。稍等片刻你就会在工具箱里看到一个名为VerticalLabelControl的新控件图标和Label一样。现在你就可以像拖放Button、TextBox一样把它拖到你的窗体上使用了。在设计器属性窗口里可以直接设置VerticalText、Font、ForeColor等属性所见即所得。我把我自己封装好的这个控件用在了好几个工业上位机软件的项目里用来显示竖直排列的设备状态指示灯标签比如“运行中”、“待机”、“故障”。以前用土办法画上去的文字在界面频繁刷新时总感觉有残影换了这套双缓冲、属性驱动的控件后显示效果非常稳定和干净再也没有收到过测试同事关于“标签显示有问题”的反馈。代码结构清晰了维护起来也省心要加个文字阴影或者渐变效果只需要改RenderToBuffer这一个地方就行。