使用 MonoDevelop 和 GTK# 的自定义控件






4.40/5 (9投票s)
在 Linux 上使用 Mono 实现可移动控件,并进行自定义渲染。
引言
通常,Mono 和 GTK 的自定义控件是使用基本控件(Widgets)完成的。有时,为了实现更高级的 GUI,会选择自定义渲染。本文介绍了一个在面板内实现可移动对象的简单实现,并带有一个弹出菜单来修改每个对象及其外观。
背景
我最近切换到了 Ubuntu,并且对此非常满意。我主要在 .NET 上进行开发,而我对某些 Windows 工具的依赖是我一直等待的原因。现在这已经不再是什么问题了,这要归功于 Mono 和 MonoDevelop 团队以及 GTK#、Cairo 和 Pango 等相关库的出色工作。
Mono 将 .NET 平台带到了 Linux,而 MonoDevelop 提供了 Visual Studio 的良好替代方案,使得在 GTK 桌面上的 GUI 应用程序开发几乎毫不费力。
我编写了一个我日常使用的协作工具,在切换平台后,我的第一个目标是将其移植到 Mono 和 GTK#。在由于 Windows Forms 和 GTK# 之间的根本差异导致的一些调整之后,我不得不承认,应用程序的移植比我最初想象的要容易得多;我用小部件替换了 UI 控件,设置了各种窗体使用 GTK# 布局,仅此而已。其余的非 UI 代码按预期工作,整体性能良好。
让我最困难的领域是使用行为与基本 GTK 组件不同的自定义控件。这也是本文的重点。
目标
- 元素的可视化表示,如同图表
- 每个元素都有一个标题和一些在渲染中显示的属性
- 每个元素都可以在定义的面板中自由移动
- 每个元素都有自己的弹出菜单来更改其属性
解决方案
在花了一些时间研究 GTK 文档后,Fixed
和 DrawingArea
小部件似乎是显而易见的 Au choix。Fixed
面板允许控件以绝对位置进行放置。尽管通常不推荐在大多数窗体中使用它,但在本例中它效果很好。
MVPanel
是继承自 Fixed
的容器,用于容纳可移动对象。它可以像普通的 GTK 小部件一样放置在任何窗体中。它包含以下方法:
AddMovingObject(string name,string caption, int x, int y)
用于添加新的可移动对象。RefreshChildren()
用于强制控件重绘。
MVObject
是自定义控件的基本实现,它将在 MVPanel
中可移动。它继承自 DrawingArea
。
它包含以下方法/属性:
ShowMenu()
向用户显示该特定控件的选项。Edit()
将控件设置为编辑模式。Redraw()
强制控件重绘。
Caption
用于获取控件的基本属性。
渲染自定义控件
DrawingArea
小部件的渲染必须完全指定。这是自定义的缺点,但有时是必需的。Cairo(用于图形)和 Pango(用于文本)库似乎是推荐使用的,以呈现一致且现代的外观。本示例使用了 Pango,但未使用 Cairo,因为渲染相当简单。
重写的 OnExposeEvent
方法是所有渲染发生的地方。在这里,区域用深灰色和浅灰色绘制,并用一些黑线分隔。在每个彩色区域中还添加了一些文本。
using System;
using Gtk;
namespace GtkControl.Control
{
public class MVObject : Gtk.DrawingArea
{
public MVObject(string pName, string cap)
{
...
}
public void Redraw()
{
this.QueueDraw();
}
private Pango.Layout GetLayout(string text)
{
Pango.Layout layout = new Pango.Layout(this.PangoContext);
layout.FontDescription = Pango.FontDescription.FromString("monospace 8");
layout.SetMarkup("<span color=\"black\">" + text + "</span>");
return layout;
}
protected override bool OnExposeEvent (Gdk.EventExpose args)
{
Gdk.Window win = args.Window;
Gdk.Rectangle area = args.Area;
win.DrawRectangle(Style.DarkGC(StateType.Normal), true, area);
win.DrawRectangle(Style.MidGC(StateType.Normal),true,0,15,1000,1000);
win.DrawRectangle(Style.BlackGC,false,area);
win.DrawLine(Style.BlackGC,0,15,1000,15);
win.DrawLayout(Style.BlackGC,2,2,titleLayout);
if (!string.IsNullOrEmpty(body))
{
win.DrawLayout(Style.BlackGC,2,17,GetLayout(body));
}
return true;
}
}
}
QueueDraw
方法是对 GTK 的调用,指示控件需要重绘。
移动控件
默认情况下,控件无法移动。但是,Fixed
小部件允许将控件放置或移动到指定位置。此外,控件需要响应鼠标单击、拖动和按钮释放。
using System;
using Gtk;
namespace GtkControl.Control
{
public partial class MVPanel : Gtk.Bin
{
private Widget currCtrl = null;
private Widget currClone = null;
private int origX = 0;
private int origY = 0;
private int pointX = 0;
private int pointY = 0;
...
//Mouse click on the controls of the panel
protected void OnButtonPressed(object sender, ButtonPressEventArgs a)
{
//Save the origin of the move in origX and origY
currCtrl = sender as Widget;
currCtrl.TranslateCoordinates(this.fixed1,0,0,out origX, out origY);
//Save the pointer position relative the origin of the move
fixed1.GetPointer(out pointX,out pointY);
}
protected void OnButtonReleased(object sender, ButtonReleaseEventArgs a)
{
//Final destination of the control
MoveControl(currCtrl, a.Event.X,a.Event.Y,false);
currCtrl = null;
if (currClone!=null)
{
this.fixed1.Remove(currClone);
currClone.Destroy();
currClone = null;
}
}
//Called whenever a control is moved
protected virtual void OnFixed1MotionNotifyEvent (object o,
Gtk.MotionNotifyEventArgs args)
{
//Rendering of a clone at the desired location
if (currCtrl!=null)
{
MoveClone(ref currClone, args.Event.X,args.Event.Y);
}
}
}
}
TranslateCoordinates
是一个 GTK 方法,它返回控件在容器(此处为 fixed1
)中的相对位置。
GetPointer
也是一个 GTK 方法,它返回光标相对于控件的位置。
MoveControl
方法调用 fixed1.Move
方法,并确保控件保留在面板内。它还负责在控件移动后重绘控件。
MoveClone
方法调用 MoveControl
对选定小部件的克隆进行操作。这确保用户在两个位置(原点和目标)都能看到控件。克隆在按下按钮时生成,并跟随鼠标移动直到按钮释放。如果不需要中间状态,可以删除 MotionNotifyEvent
事件。
DrawingArea
对象进行渲染,但不响应事件。这就是为什么 MVObject
被嵌入到 EventBox
中,所有鼠标事件都在其中进行控制。这在 GetMovingBox
方法中完成。
//Create the event box for the custom control
private EventBox GetMovingBox(string name, string caption)
{
MVObject ctrl = new MVObject(name,caption);
EventBox rev = new EventBox();
rev.Name = name;
rev.Add(ctrl);
Console.WriteLine("Creating new moving object"+rev.Name);
return rev;
}
使用 MVPanel
公共方法 AddMovingObject(string name,string caption, int x, int y)
是将对象添加到面板的方式,其中 caption 是标题,name 是对象的标识。
//Add a movable control to the panel
public void AddMovingObject(string name,string caption, int x, int y)
{
//Prevent the object to be displayed outside the panel
if (x<0)
{
x = 0;
}
if (y<0)
{
y = 0;
}
//Create the box where the custom object is rendered
EventBox ev = GetMovingBox(name,caption);
//Add the events to control the movement of the box
ev.ButtonPressEvent+=new ButtonPressEventHandler(OnButtonPressed);
ev.ButtonReleaseEvent+=new ButtonReleaseEventHandler(OnButtonReleased);
//Add the control to the panel
this.fixed1.Put(ev,x,y);
this.ShowAll();
}
对于上面的截图,它翻译为:
this.mvpanel1.AddMovingObject("mv1","Moving Object 1",10,10);
this.mvpanel1.AddMovingObject("mv2","Moving Object 2",10,55);
this.mvpanel1.AddMovingObject("mv3","Mono",10,100);
this.mvpanel1.AddMovingObject("mv4","Gtk#",10,145);
this.mvpanel1.AddMovingObject("mv5","MonoDevelop",10,190);
this.mvpanel1.AddMovingObject("mv6","Pango",10,235);
this.mvpanel1.AddMovingObject("mv7","Test",10,280);
在控件上显示上下文菜单
示例的最后一部分,控件的上下文菜单。
MVObject
实现了一个公共方法 ShowMenu
,它简单地调用预定义的 Gtk.Menu
在控件上的 Popup
方法。
Gtk.Menu popup = new Gtk.Menu();
Gtk.MenuItem text1 = new MenuItem("Test1");
text1.Activated+=new EventHandler(Menu1Clicked);
Gtk.MenuItem text2 = new MenuItem("Test2");
text2.Activated+=new EventHandler(Menu2Clicked);
Menu1Clicked
更改控件上的 body
属性并强制重绘。这通过 OnExposedEvent
方法改变了其外观。
protected void Menu1Clicked(object sender, EventArgs args)
{
body = "Test1";
this.QueueDraw();
}
菜单的调用在 MVPanel.OnButtonPress
中由鼠标右键单击定义。
//Mouse click on the controls of the panel
protected void OnButtonPressed(object sender, ButtonPressEventArgs a)
{
//Right click
if (a.Event.Button==3)
{
if (sender is EventBox)
{
((sender as EventBox).Child as MVObject).ShowMenu();
}
}
//Left click
else if (a.Event.Button==1)
{
//Double-click
if (a.Event.Type==Gdk.EventType.TwoButtonPress)
{
if (sender is EventBox)
{
//Calling the edit method of the control
((sender as EventBox).Child as MVObject).Edit();
}
}
else
{
//Setup the origin of the move
currCtrl = sender as Widget;
currCtrl.TranslateCoordinates(this.fixed1,0,0,out origX, out origY);
fixed1.GetPointer(out pointX,out pointY);
}
}
}
关注点
这个实现比我预期的要容易。当然,它目前的状态并不是非常有用,但它让我对在常规小部件之外使用 GTK 和 Mono 的可能性有了很好的认识。我可能会将其扩展为我自己的小型图形框架,但我确信它已经以单独项目的形式存在,并且设计更健壮。但是,经过一些额外的调整,它肯定会比我需要的多。
我想分享这段代码,因为我发现关于自定义渲染实现、带有完整代码的文档在 Mono 上非常少。
但是,关于我用作参考的基础库,有很多文档: