CPF/CPF.Linux/LinuxPlatform.cs

789 lines
30 KiB
C#
Raw Permalink Normal View History

2023-11-21 23:05:03 +08:00
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Runtime.InteropServices;
using CPF;
using CPF.Drawing;
using CPF.Input;
using CPF.Platform;
using System.Threading.Tasks;
using CPF.Controls;
using System.Linq;
using System.Diagnostics;
using static CPF.Linux.XLib;
using static CPF.Linux.Glib;
using static CPF.Linux.Gtk;
using System.Net.Sockets;
using System.Net;
using System.IO;
namespace CPF.Linux
{
public class LinuxPlatform : RuntimePlatform
{
public static LinuxPlatform Platform;
////[DllImport("libXlibDemo.so")]
////public static extern IntPtr OpenIM(IntPtr dpy);
////[DllImport("libXlibDemo.so")]
////public static extern IntPtr CreateIC(IntPtr xim, IntPtr win);
//[DllImport("libXlibDemo.so")]
//public static extern void Move(IntPtr win, IntPtr dpy, ref XEvent xEvent);
///// <summary>
///// https://blog.csdn.net/qq_32768743/article/details/90605212
///// </summary>
//public static void Test()
//{
// Console.WriteLine(setlocale(6, ""));
// var dpy = XOpenDisplay(IntPtr.Zero);
// XSupportsLocale();
// Console.WriteLine(XSetLocaleModifiers(""));
// IntPtr default_string = IntPtr.Zero;
// var fontset = XCreateFontSet(dpy, "-*-*-medium-r-normal-*-*-120-*-*-*-*", out var missing_charsets, out var num_missing_charsets, ref default_string);
// Console.WriteLine("default_string:" + Marshal.PtrToStringUni(default_string));
// //Thread.Sleep(10000);
// var screen = XDefaultScreen(dpy);
// var win = XCreateSimpleWindow(dpy, XRootWindow(dpy, screen), 0, 0, 500, 200,
// 2, XBlackPixel(dpy, screen), XWhitePixel(dpy, screen));
// var gc = XCreateGC(dpy, win, 0, IntPtr.Zero);
// XSetForeground(dpy, gc, XWhitePixel(dpy, screen));
// XSetBackground(dpy, gc, XBlackPixel(dpy, screen));
// var im = XOpenIM(dpy, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
// var list = XVaCreateNestedList(0, XNames.XNFontSet, fontset, IntPtr.Zero);
// var best_style = XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing;
// Console.WriteLine(string.Join("-", X11Window.GetSupportedInputStyles(im)));
// var ic = XCreateIC(im,
// XNames.XNInputStyle, best_style,
// XNames.XNClientWindow, win,
// IntPtr.Zero);
// XFree(list);
// long im_event_mask = 0;
// XGetICValues(ic, XNames.XNFilterEvents, ref im_event_mask, IntPtr.Zero);
// XSelectInput(dpy, win, (IntPtr)((long)(XEventMask.ExposureMask | XEventMask.KeyPressMask
// | XEventMask.StructureNotifyMask) | im_event_mask));
// XSetICFocus(ic);
// XMapWindow(dpy, win);
// //XRectangle preedit_area = new XRectangle();
// //XRectangle status_area = new XRectangle();
// while (true)
// {
// XNextEvent(dpy, out var xEvent);
// if (XFilterEvent(ref xEvent, IntPtr.Zero))
// continue;
// //switch (xEvent.type)
// //{
// // case XEventName.KeyPress:
// // break;
// // case XEventName.ConfigureNotify:
// // if ((best_style & XIMProperties.XIMPreeditArea) != 0)
// // {
// // preedit_area.width = (ushort)(xEvent.ConfigureEvent.width * 4 / 5);
// // preedit_area.height = 0;
// // GetPreferredGeometry(ic, XNames.XNPreeditAttributes, ref preedit_area);
// // preedit_area.x = (short)(xEvent.ConfigureEvent.width - preedit_area.width);
// // preedit_area.y = (short)(xEvent.ConfigureEvent.height - preedit_area.height);
// // SetGeometry(ic, XNames.XNPreeditAttributes, preedit_area);
// // }
// // if ((best_style & XIMProperties.XIMStatusArea) != 0)
// // {
// // status_area.width = (ushort)(xEvent.ConfigureEvent.width / 5);
// // status_area.height = 0;
// // GetPreferredGeometry(ic, XNames.XNStatusAttributes, ref status_area);
// // status_area.x = 0;
// // status_area.y = (short)(xEvent.ConfigureEvent.height - status_area.height);
// // SetGeometry(ic, XNames.XNStatusAttributes, status_area);
// // }
// // break;
// // default:
// // break;
// //}
// }
//}
//static void GetPreferredGeometry(IntPtr ic, string name, ref XRectangle area)
////XIC ic;
////char *name; /* XNPreEditAttributes or XNStatusAttributes */
////XRectangle *area; /* the constraints on the area */
//{
// var list = XVaCreateNestedList(0, "areaNeeded", ref area, IntPtr.Zero);
// /* set the constraints */
// XSetICValues(ic, name, list, IntPtr.Zero);
// /* Now query the preferred size */
// /* The Xsi input method, Xwnmo, seems to ignore the constraints, */
// /* but were not going to try to enforce them here. */
// XGetICValues(ic, name, list, IntPtr.Zero);
// XFree(list);
//}
////XIC ic;
////char *name; /* XNPreEditAttributes or XNStatusAttributes */
////XRectangle *area; /* the actual area to set */
//static unsafe void SetGeometry(IntPtr ic, string name, XRectangle area)
//{
// var list = XVaCreateNestedList(0, "area", ref area, IntPtr.Zero);
// XSetICValues(ic, name, list, IntPtr.Zero);
// XFree(list);
//}
enum EventCodes
{
X11 = 1,
Signal = 2
}
//private int _sigread, _sigwrite;
//private int _epoll;
private object _lock = new object();
//private bool _signaled;
static Pollfd[] pollfds; // For watching the X11 socket
static Socket listen; //
static Socket wake; //
static Socket wake_receive; //
static byte[] network_buffer; //
/// <summary>
/// 启用触摸事件之所有加上这个因为部分Linux对XI2支持有问题
/// </summary>
public bool EnabledTouch { get; set; }
public unsafe LinuxPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Console.WriteLine($"系统架构:{RuntimeInformation.OSArchitecture}");
Console.WriteLine($"系统名称:{RuntimeInformation.OSDescription}");
Console.WriteLine($"进程架构:{RuntimeInformation.ProcessArchitecture}");
Console.WriteLine($"是否64位操作系统{Environment.Is64BitOperatingSystem}");
Console.WriteLine("CPU CORE:" + Environment.ProcessorCount);
Console.WriteLine("HostName:" + Environment.MachineName);
Console.WriteLine("Version:" + Environment.OSVersion);
Console.WriteLine("CPF Version:" + typeof(LinuxPlatform).Assembly.GetName().Version);
Platform = this;
XInitThreads();
Display = XOpenDisplay(IntPtr.Zero);
//Console.WriteLine(setlocale(6, ""));
//DeferredDisplay = XOpenDisplay(IntPtr.Zero);
if (Display == IntPtr.Zero)
throw new Exception("XOpenDisplay failed");
XError.Init();
Info = new X11Info(Display, DeferredDisplay);
Thread.CurrentThread.Name = "主线程";
_cursors = Enum.GetValues(typeof(CursorFontShape)).Cast<CursorFontShape>()
.ToDictionary(id => id, id => XLib.XCreateFontCursor(Display, id));
if (Info.XInputVersion != null && EnabledTouch)
{
var xi2 = new XI2Manager();
if (xi2.Init(this))
XI2 = xi2;
}
var fd = XLib.XConnectionNumber(Display);
//var ev = new epoll_event()
//{
// events = EPOLLIN,
// data = { u32 = (int)EventCodes.X11 }
//};
//_epoll = epoll_create1(0);
//if (_epoll == -1)
// throw new Exception("epoll_create1 failed");
//if (epoll_ctl(_epoll, EPOLL_CTL_ADD, fd, ref ev) == -1)
// throw new Exception("Unable to attach X11 connection handle to epoll");
//var fds = stackalloc int[2];
////pipe2(fds, O_NONBLOCK);
//if (pipe2(fds, O_NONBLOCK) == -1)
// throw new Exception("Unable to create X11 pipe");
//_sigread = fds[0];
//_sigwrite = fds[1];
//ev = new epoll_event
//{
// events = EPOLLIN,
// data = { u32 = (int)EventCodes.Signal }
//};
//if (epoll_ctl(_epoll, EPOLL_CTL_ADD, _sigread, ref ev) == -1)
// throw new Exception("Unable to attach signal pipe to epoll");
// For sleeping on the X11 socket
listen = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
IPEndPoint ep = new IPEndPoint(IPAddress.Loopback, 0);
listen.Bind(ep);
listen.Listen(1);
// To wake up when a timer is ready
network_buffer = new byte[10];
wake = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
wake.Connect(listen.LocalEndPoint);
// Make this non-blocking, so it doesn't
// deadlock if too many wakes are sent
// before the wake_receive end is polled
wake.Blocking = false;
wake_receive = listen.Accept();
pollfds = new Pollfd[2];
pollfds[0] = new Pollfd();
pollfds[0].fd = fd;
pollfds[0].events = PollEvents.POLLIN;
pollfds[1] = new Pollfd();
pollfds[1].fd = wake_receive.Handle.ToInt32();
pollfds[1].events = PollEvents.POLLIN;
XrmInitialize(); /* Need to initialize the DB before calling Xrm* functions */
}
}
public XI2Manager XI2;
public X11Info Info { get; private set; }
public IntPtr DeferredDisplay { get; set; }
public IntPtr Display { get; set; }
public override PixelPoint MousePosition
{
get
{
var po = GetCursorPos(Info);
//Console.WriteLine(po);
return new PixelPoint(po.x, po.y);
}
}
public override TimeSpan DoubleClickTime
{
get
{
return TimeSpan.FromSeconds(0.4);
}
}
public override IPopupImpl CreatePopup()
{
return new PopWindow();
}
public override IWindowImpl CreateWindow()
{
return new X11Window();
}
DataObject dataObject;
public override DragDropEffects DoDragDrop(DragDropEffects allowedEffects, params (DataFormat, object)[] data)
{
dataObject = new DataObject();
dataObject.StartDrag(allowedEffects, data);
return allowedEffects;
}
public override IReadOnlyList<Screen> GetAllScreen()
{
return ScreenImpl.Screens;
}
public override IClipboard GetClipboard()
{
return new X11Clipboard();
}
private Dictionary<CursorFontShape, IntPtr> _cursors;
private static readonly Dictionary<Cursors, CursorFontShape> s_mapping =
new Dictionary<Cursors, CursorFontShape>
{
{Cursors.Arrow, CursorFontShape.XC_top_left_arrow},
{Cursors.Cross, CursorFontShape.XC_cross},
{Cursors.Hand, CursorFontShape.XC_hand1},
{Cursors.Help, CursorFontShape.XC_question_arrow},
{Cursors.Ibeam, CursorFontShape.XC_xterm},
{Cursors.No, CursorFontShape.XC_X_cursor},
{Cursors.Wait, CursorFontShape.XC_watch},
{Cursors.AppStarting, CursorFontShape.XC_watch},
{Cursors.BottomSide, CursorFontShape.XC_bottom_side},
{Cursors.DragCopy, CursorFontShape.XC_center_ptr},
{Cursors.DragLink, CursorFontShape.XC_fleur},
{Cursors.DragMove, CursorFontShape.XC_diamond_cross},
{Cursors.LeftSide, CursorFontShape.XC_left_side},
{Cursors.RightSide, CursorFontShape.XC_right_side},
{Cursors.SizeAll, CursorFontShape.XC_sizing},
{Cursors.TopSide, CursorFontShape.XC_top_side},
{Cursors.UpArrow, CursorFontShape.XC_sb_up_arrow},
{Cursors.BottomLeftCorner, CursorFontShape.XC_bottom_left_corner},
{Cursors.BottomRightCorner, CursorFontShape.XC_bottom_right_corner},
{Cursors.SizeNorthSouth, CursorFontShape.XC_sb_v_double_arrow},
{Cursors.SizeWestEast, CursorFontShape.XC_sb_h_double_arrow},
{Cursors.TopLeftCorner, CursorFontShape.XC_top_left_corner},
{Cursors.TopRightCorner, CursorFontShape.XC_top_right_corner},
};
public override object GetCursor(Cursors cursorType)
{
IntPtr handle;
//if (cursorType == Cursors.None)
//{
// handle = _nullCursor;
//}
//else
//{
handle = s_mapping.TryGetValue(cursorType, out var shape)
? _cursors[shape]
: _cursors[CursorFontShape.XC_top_left_arrow];
//}
return handle;
}
public override SynchronizationContext GetSynchronizationContext()
{
return new LinuxSynchronizationContext();
}
static Dictionary<KeyGesture, PlatformHotkey> keyValuePairs = new Dictionary<KeyGesture, PlatformHotkey>() {
{ new KeyGesture(Keys.C,InputModifiers.Control),PlatformHotkey.Copy},
{ new KeyGesture(Keys.X,InputModifiers.Control),PlatformHotkey.Cut},
{ new KeyGesture(Keys.V,InputModifiers.Control),PlatformHotkey.Paste},
{ new KeyGesture(Keys.Y,InputModifiers.Control),PlatformHotkey.Redo},
{ new KeyGesture(Keys.A,InputModifiers.Control),PlatformHotkey.SelectAll},
{ new KeyGesture(Keys.Z,InputModifiers.Control),PlatformHotkey.Undo},
};
public override PlatformHotkey Hotkey(KeyGesture keyGesture)
{
keyValuePairs.TryGetValue(keyGesture, out PlatformHotkey platformHotkey);
return platformHotkey;
}
internal Dictionary<IntPtr, XWindow> windows = new Dictionary<IntPtr, XWindow>();
internal bool run = true;
public unsafe override void Run()
{
//X11Window x11Window = new X11Window();
//x11Window.SetVisible(true);
DoTask();
while (run)
{
XSync(Display, false);
while (XPending(Display) != 0)
{
XNextEvent(Display, out var xev);
if (XFilterEvent(ref xev, IntPtr.Zero))
{
continue;
}
OnEvent(ref xev, null);
}
//Invoke();
//Thread.Sleep(1);
//lock (_lock)
//{
// _signaled = false;
// int buf = 0;
// while (read(_sigread, &buf, new IntPtr(4)).ToInt64() > 0)
// {
// }
//}
DoTask();
Wait();
}
XCloseDisplay(Display);
}
public override void Run(CancellationToken cancellation)
{
//Console.WriteLine("开始循环");
while (!cancellation.IsCancellationRequested && run)
{
XSync(Display, false);
while (XPending(Display) != 0)
{
XNextEvent(Display, out var xev);
if (XFilterEvent(ref xev, IntPtr.Zero))
{
continue;
}
OnEvent(ref xev, null);
}
DoTask();
if (cancellation.IsCancellationRequested)
{
break;
}
Wait();
}
//Console.WriteLine("结束循环" + cancellation.IsCancellationRequested + run);
}
private unsafe void Wait()
{
XFlush(Display);
if (XPending(Display) == 0)
{
//if (run)
//{
//epoll_event ev;
// epoll_wait(_epoll, &ev, 1, -1);//-1永久阻塞直到有信号,
//}
poll(pollfds, (uint)pollfds.Length, -1);
// Clean out buffer, so we're not busy-looping on the same data
//if (length == pollfds.Length)
{
if (pollfds[1].revents != 0)
wake_receive.Receive(network_buffer, 0, 1, SocketFlags.None);
}
}
CPF.Threading.DispatcherTimer.SetTimeTick();
}
HashSet<XEventHandler> handlers = new HashSet<XEventHandler>();
public unsafe void RunMainLoop(CancelHandle cancelHandle, XEventHandler action)
{
handlers.Add(action);
while (!cancelHandle.Cancel && run)
{
XSync(Display, false);
while (XPending(Display) != 0)
{
XNextEvent(Display, out var xev);
if (XFilterEvent(ref xev, IntPtr.Zero))
{
continue;
}
if (!action(ref xev))
{
OnEvent(ref xev, action);
}
}
//Invoke();
//Thread.Sleep(1);
//lock (_lock)
//{
// _signaled = false;
// int buf = 0;
// while (read(_sigread, &buf, new IntPtr(4)).ToInt64() > 0)
// {
// }
//}
DoTask();
Wait();
}
handlers.Remove(action);
}
internal unsafe void SetFlag()
{
//lock (_lock)
//{
// if (_signaled)
// return;
// _signaled = true;
// int buf = 0;
// write(_sigwrite, &buf, new IntPtr(1));
//}
try
{
wake.Send(new byte[] { 0xFF });
}
catch (SocketException ex)
{
if (ex.SocketErrorCode != SocketError.WouldBlock)
{
throw;
}
}
}
void Invoke()
{
if (LinuxSynchronizationContext.asyncQueue.TryDequeue(out SendOrPostData postData))
{
postData.SendOrPostCallback(postData.Data);
}
if (LinuxSynchronizationContext.invokeQueue.TryDequeue(out SendOrPostData result))
{
result.SendOrPostCallback(result.Data);
result.ManualResetEvent.Set();
}
}
unsafe void OnEvent(ref XEvent xev, XEventHandler action)
{
foreach (var item in handlers.Reverse())
{
if (action != item)
{
if (item(ref xev))
{
return;
}
}
}
//if (xev.type == XEventName.GenericEvent)
// fixed (void* data = &xev.GenericEventCookie)
// {
// XGetEventData(Display, data);
// }
if (windows.TryGetValue(xev.AnyEvent.window, out var window))
{
lock (XWindow.XlibLock)
{
window.OnEvent(ref xev);
}
}
else if (xev.type == XEventName.GenericEvent && LinuxPlatform.Platform.XI2 != null)
{
fixed (void* data = &xev.GenericEventCookie)
{
XGetEventData(Info.Display, data);
try
{
if (Info.XInputOpcode ==
xev.GenericEventCookie.extension)
{
//Console.WriteLine((IntPtr)ev.GenericEventCookie.data);
var ev = (XIEvent*)xev.GenericEventCookie.data;
if (windows.TryGetValue(((XIDeviceEvent*)ev)->EventWindow, out window))
{
Platform.XI2.OnEvent(ev, (X11Window)window);
}
}
}
finally
{
if (xev.GenericEventCookie.data != null)
XFreeEventData(Info.Display, data);
}
}
}
//}
//finally
//{
// if (xev.type == XEventName.GenericEvent && xev.GenericEventCookie.data != null)
// XFreeEventData(Display, &xev.GenericEventCookie);
//}
}
private static unsafe void DoTask()
{
if (LinuxSynchronizationContext.invokeQueue.Count > 0)
{
while (LinuxSynchronizationContext.invokeQueue.TryDequeue(out var result))
{
result.SendOrPostCallback(result.Data);
result.ManualResetEvent.Set();
}
}
if (LinuxSynchronizationContext.asyncQueue.Count > 0)
{
while (LinuxSynchronizationContext.asyncQueue.TryDequeue(out SendOrPostData data))
{
data.SendOrPostCallback(data.Data);
}
}
}
void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
{
var xid = ((X11Window)parentWindow).Handle;
gtk_widget_realize(chooser);
var window = gtk_widget_get_window(chooser);
var parent = GetForeignWindow(xid);
if (window != IntPtr.Zero && parent != IntPtr.Zero)
gdk_window_set_transient_for(window, parent);
}
async Task EnsureInitialized()
{
if (_initialized == null) _initialized = StartGtk();
if (!(await _initialized))
throw new Exception("Unable to initialize GTK on separate thread");
}
private Task<bool> _initialized;
private unsafe Task<string[]> ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, string initialFileName, IEnumerable<FileDialogFilter> filters)
{
IntPtr dlg;
using (var name = new Utf8Buffer(title))
dlg = gtk_file_chooser_dialog_new(name, IntPtr.Zero, action, IntPtr.Zero);
UpdateParent(dlg, parent);
if (multiSelect)
gtk_file_chooser_set_select_multiple(dlg, true);
gtk_window_set_modal(dlg, true);
var tcs = new TaskCompletionSource<string[]>();
List<IDisposable> disposables = null;
if (filters != null)
foreach (var f in filters.Where(a => !string.IsNullOrWhiteSpace(a.Extensions)))
{
var filter = gtk_file_filter_new();
using (var b = new Utf8Buffer(f.Name))
gtk_file_filter_set_name(filter, b);
foreach (var e in f.Extensions.Split(',').Where(a => !string.IsNullOrWhiteSpace(a)).Select(a => a.Trim()))
using (var b = new Utf8Buffer("*." + e))
gtk_file_filter_add_pattern(filter, b);
gtk_file_chooser_add_filter(dlg, filter);
}
disposables = new List<IDisposable>
{
ConnectSignal<signal_generic>(dlg, "close", delegate
{
tcs.TrySetResult(null);
foreach (var d in disposables) d.Dispose();
disposables.Clear();
return false;
}),
ConnectSignal<signal_dialog_response>(dlg, "response", (_, resp, __) =>
{
string[] result = null;
if (resp == GtkResponseType.Accept)
{
var resultList = new List<string>();
var gs = gtk_file_chooser_get_filenames(dlg);
var cgs = gs;
while (cgs != null)
{
if (cgs->Data != IntPtr.Zero)
resultList.Add(Utf8Buffer.StringFromPtr(cgs->Data));
cgs = cgs->Next;
}
g_slist_free(gs);
result = resultList.ToArray();
}
gtk_widget_hide(dlg);
foreach (var d in disposables) d.Dispose();
disposables.Clear();
tcs.TrySetResult(result);
return false;
})
};
using (var open = new Utf8Buffer(
action == GtkFileChooserAction.Save ? "Save"
: action == GtkFileChooserAction.SelectFolder ? "Select"
: "Open"))
gtk_dialog_add_button(dlg, open, GtkResponseType.Accept);
using (var open = new Utf8Buffer("Cancel"))
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
if (initialFileName != null)
using (var fn = new Utf8Buffer(initialFileName))
{
if (action == GtkFileChooserAction.Save)
gtk_file_chooser_set_current_name(dlg, fn);
else
gtk_file_chooser_set_filename(dlg, fn);
}
gtk_window_present(dlg);
return tcs.Task;
}
string NameWithExtension(string path, string defaultExtension, FileDialogFilter filter)
{
var name = Path.GetFileName(path);
if (name != null && !name.Contains("."))
{
if (!string.IsNullOrWhiteSpace(filter?.Extensions))
{
if (defaultExtension != null
&& filter.Extensions.Contains(defaultExtension))
return path + "." + defaultExtension.TrimStart('.');
var ext = filter.Extensions.Split(',').FirstOrDefault(x => x != "*");
if (ext != null)
return path + "." + ext.TrimStart('.');
}
if (defaultExtension != null)
path += "." + defaultExtension.TrimStart('.');
}
return path;
}
public override async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
{
await EnsureInitialized();
return await await RunOnGlibThread(
() => ShowDialog(dialog.Title, parent,
dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
(dialog as OpenFileDialog)?.AllowMultiple ?? false,
System.IO.Path.Combine(string.IsNullOrEmpty(dialog.Directory) ? "" : dialog.Directory,
string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters));
}
public override async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(dialog.Title, parent,
GtkFileChooserAction.SelectFolder, false, dialog.Directory, null);
return res?.FirstOrDefault();
});
}
public override INativeImpl CreateNative()
{
return new NativeHost();
}
public override INotifyIconImpl CreateNotifyIcon()
{
return new NotifyIcon();
}
}
/// <summary>
/// 处理事件返回值为true的时候不调用默认处理方法
/// </summary>
/// <param name="xEvent"></param>
/// <returns></returns>
public delegate bool XEventHandler(ref XEvent xEvent);
public class CancelHandle
{
public bool Cancel { get; set; }
public object Data { get; set; }
/// <summary>
/// 设置数据同时设置Cancel=true
/// </summary>
/// <param name="data"></param>
public void SetResult(object data)
{
Data = data;
Cancel = true;
}
}
}