FrontEnd под Win32 на C# своими руками

1. Введение

Как вам в одной из своих статей, посвященных программированию под Linux, абсолютно верно рассказал Дмитрий Бушенко, frontend предоставляет UI к более сложной в использовании программе. Последнее время мне часто приходилось работать с десятичными и шестнадцатеричными числами. Чтобы переводить числа из одной системы счисления в другую, я использовал просто замечательную в своем роде консольную утилиту Марка Руссиновича hex2dec (официальный сайт: сайт ), которая плюс ко всему еще и самостоятельно определяет формат числа на входе. Я было начал работать с нею в консоли, но почти сразу же почувствовал, как сильно упала моя производительность. Поэтому я решил, не откладывая в долгий ящик, написать "навороченную" обвязку для hex2dec на Visual C# 2005.

2. Вася, заводи! Поехали...

Мне кажется, что уже при таких объемах работ гораздо разумнее будет привести весь код целиком, а затем уже ссылаться на него по ходу дела. Все выкладки я, как и обычно, обильно снабжаю комментариями, так что вполне возможно, что вам все станет ясно лишь при беглом просмотре кода. Тем не менее, я постарался сочинить для вас увлекательный рассказ. Начнем по возрастающей.

1 2.1. ListBox + Timer

Для разминочки напишем свой ListBox, который автоматически выделяет тот элемент списка, который в данный момент находится под указателем мыши. Как это часто бывает с .NET Framework, если хочется реализовать то, что не особенно-то и нужно, но иногда очень хочется, приходится обращаться напрямую к WinAPI. В данном случае идея следующая. По таймеру с интервалом 1 мс с помощью функции GetCursorPos определяем текущее положение указателя мыши в экранных координатах, затем преобразовываем их в клиентские, после чего средствами самого ListBox'a подсвечиваем элемент из списка, соответствующий этим координатам. Как ни странно, на загрузку процессора это не оказывает совершенно никакого влияния. Функция GetCursorPos описана в библиотеке user32.dll. Чтобы указать CLR, что мы хотим импортировать функцию из этой библиотеки, используется атрибут DllImportAttribute из пространства имен System.Runtime.InteropServices. У меня в коде объявление GetCursorPos идет начиная с 34 строки. В качестве параметра этот метод принимает ссылку на структуру POINT. Но т.к. в .NET Framework нету идентичной структуры, придется реализовать ее отдельно (строки 40-52). Обращаю ваше внимание на атрибут StructLayoutAttribute. В конструктор этого атрибута передается перечисление LayoutKind, которое определяет, каким образом члены структуры (или класса) должны быть расположены в памяти. При обращении к неуправляемому коду все поля структуры должны находиться в последовательности, определенной разработчиком, что достигается при LayoutKind.Sequential.

1 using System;
2 using System.Drawing;
3 using System.Runtime.InteropServices;
4 using System.Windows.Forms;
5
6 namespace hex2dec
7 {
8 /// <summary>
9 /// Улучшенный ListBox
10 /// </summary>
11 public class MyListBox : ListBox
12 {
13 /// <summary>
14 /// По таймеру получаем позицию курсора.
15 /// </summary>
16 private Timer timer = new Timer();
17
18 /// <summary>
19 /// Индекс предыдущего выделенного элемента списка. Используется в качестве текущего,
20 /// когда реальный индекс равен -1, т.е. когда указатель мыши находится вне зоны ListBox'a.
21 /// </summary>
22 private int bufferIndex = 0;
23
24 /// <summary>
25 /// Определяет x- и y-координты указателя мыши.
26 /// </summary>
27 private POINT lpPoint = new POINT();
28
29 /// <summary>
30 /// Получает экранные координаты указателя мыши.
31 /// </summary>
32 /// <param name="lpPoint">Ссылка на структуру, которая получает координаты указателя мыши.</param>
33 /// <returns>Возвращает <c>true</c>, если операция закончилась успешно, иначе — <c>false</c>.</returns>
34 [DllImport("user32.dll")]
35 private static extern bool GetCursorPos(ref POINT lpPoint);
36
37 /// <summary>
38 /// Определяет x- и y-координты указателя мыши.
39 /// </summary>
40 [StructLayout(LayoutKind.Sequential)]
41 private struct POINT
42 {
43 /// <summary>
44 /// x-координата.
45 /// </summary>
46 public int x;
47
48 /// <summary>
49 /// y-координата.
50 /// </summary>
51 public int y;
52 }
53
54 /* Как только был выделен один из элементов списка, начинаем отслеживание координат указателя мыши. */
55 protected override void OnSelectedIndexChanged(EventArgs e)
56 {
57 this.timer.Enabled = true;
58 }
59
60 private void timer_Tick(object sender, EventArgs e)
61 {
62 /* Получаем экранные координаты указателя мыши. */
63 GetCursorPos(ref lpPoint);
64
65 /* Преобразовываем экранные координаты в клиентские. */
66 Point bufferPoint = this.PointToClient(new Point(lpPoint.x, lpPoint.y));
67
68 /*
69 * Если под курсором находится какой-нибудь элемент списка,
70 * получаем его индекс, иначе используем индекс предыдущего
71 * выделенного элемента.
72 */
73 if (this.IndexFromPoint(bufferPoint) != -1)
74 this.bufferIndex = this.IndexFromPoint(bufferPoint);
75
76 /* Выделяем элемент списка, соответствующий текущей позиции указателя мыши. */
77 this.SelectedIndex = this.bufferIndex;
78 }
79
80 /// <summary>
81 /// Инициализирует новый экземпляр класса <c>MyListBox</c>.
82 /// </summary>
83 public MyListBox()
84 {
85 /* Timer */
86 this.timer.Interval = 1;
87 this.timer.Tick += new EventHandler(timer_Tick);
88
89 /* ListBox */
90 this.HorizontalScrollbar = true;
91 }
92 }
93 }

2 2.2. Графический интерфейс + бизнес-логика

Здесь ситуация со встроенными функциональными возможностями .NET Framework еще хуже. Поэтому я, вспомнив, как давным-давно писал на С, перешел на голую обработку сообщений, посылаемых системой в оконную процедуру.

Как видите, в конструкторе (294-339) у нас не происходит ничего интересного, поэтому сразу перейдем к свойству CreateParams (274-288). Свойство это очень полезно и, по сути, является аналогом WinAPI функции CreateWindowEx, что дает возможность напрямую обращаться к стилям окна, а также другим, менее существенным в управляемом коде, параметрам. В теле перегрузки этого свойства я посредством бинарного оператора ИЛИ добавляю в коллекцию стиль WS_VISIBLE. Это приводит к тому, что при загрузке формы со свойством ShowInTaskBar, выставленным в false, на панели задач не остается вдавленных кнопок. По умолчанию же вдавленной остается кнопка, ассоциирующаяся с последним активным окном в системе, что не очень удобно: ведь при щелчке по этой кнопке окно, вместо того чтобы перейти на первый план, сворачивается. Особенность такого применения стиля WS_VISIBLE я обнаружил совершенно случайно, когда пытался решить аналогичную задачу, поставленную одним из посетителей форумов на GotDotNet.ru. Как оказалось, решал не зря. Вообще же, как вы, наверное, уже заметили, мы часто обращаемся к стилям и сообщениям Windows. Вы, возможно, хотите меня спросить, откуда же я беру значения этих констант? Давайте по порядку. Во время экспериментов у меня под рукой лежит Spy++ (возможно, у меня найдется немного времени, и я поведаю вам про особенности его использовании при программировании под .NET) и MSDN. Из этих источников я узнаю, как конкретные сообщения и стили влияют на поведение окна. Выбрав нужный стиль (или сообщение), я иду в директорию C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\Include, где Total Commander'ом ищу заголовочные файлы (*.h) по содержимому на предмет присутствия нужного мне стиля (или сообщения). С 99% уверенностью в найденном файле вы найдете значение этой константы. Теперь можете ее прописывать в коде явным образом. Возможно, вам повезет, и в конце описания из MSDN в разделе требований будет точно указан именно тот заголовочный файл, где имеется значение нужной вам константы. Сам же я уже давно смастерил себе библиотечку (и рекомендую вам поступить точно так же, если вы до сих пор этого не сделали), куда запихал объявления всех часто используемых мною функций, сообщений и стилей. Все, что нужно сделать при работе над новым приложением — подключить существующий проект.

Расправившись с инициализацией, перейдем к собственно разбору работы приложения. На форме у нас имеется текстовое поле, кнопка для начала конвертирования и список, куда выводится история моих конвертаций. Давайте присмотримся к методу FrontEnd (153-204). По нажатии на кнопку мы запускаем консольное приложение hex2dec и в качестве аргумента передаем ему строковое представление числа. Но сама консоль не появляется, а вывод из консоли мы перехватываем и обрабатываем средствами FCL (благо здесь они не подкачали). Т.к. вместе с результатом вычислений идет много лишнего мусора, жизненно необходимо его как-то отбросить. Я выбрал путь наименьшего сопротивления: пропустив пять строчек, считываю шестую — в ней и лежит результат. Эстеты и любители регулярных выражений могут попенять меня, мол, неаккуратненько вышло. На что я им могу ответить, что самоизысканиями мы займемся в другой раз, и лучше потратим сэкономленное время на нечто более грандиозное, чем отлаживание RegEx'ов. А замахнулся я вот на какую штуку. Мне хотелось, чтобы моя утилита сама отлавливала изменения в буфере обмена, и, если там находится строка, пыталась осуществить конвертацию. Как раз во время написания этих строк мне в голову пришла идея, что неплохо было бы возвращать результат обратно в буфер. Что ж, все в ваших (да и моих тоже) руках.

Ч. Петцольд в своей книге Programming Microsoft Windows with C# нам предлагает раз в 1 мс проверять содержимое clipboard'a. Этот вариант меня не устроил: в таком случае приложение просто задохнулось бы возвращаемыми значениями. Поэтому, переопределив оконную процедуру WndProc, я пошел по давно проторенному пути — отлавливанию сообщения WM_DRAWCLIPBOARD, которое рассылается системой при изменении содержимого буфера обмена. Эта схема работает следующим образом. Все окна, желающие получать уведомления об изменениях в буфере обмена, выстраиваются в т.н. буферную цепь (clipboard chain). Чтобы встать в эту цепь, окно вызывает функцию SetClipboardViewer, где параметром является указатель на наше окно. Эта функция возвращает указатель на следующее окно в цепочке. Предположим, одно из окон решило покинуть строй. В этом случае система рассылает сообщение WM_CHANGECBCHAIN. Получив его, наше окно по указателю смотрит, не является ли следующее за ним окно как раз тем, которое хочет уйти. Если это так, то отпускает его и берет за ручку другое рядом стоящее окно. В ином случае — пробрасывает это сообщение дальше по цепочке. Если уже наше собственное окно выходит из цепочки (например, по закрытии приложения), извещаем всех остальных об этом прискорбном событии, чтобы они смогли осуществить процедуру восстановления цепи, но уже без нашего участия.
Как только дело дойдет до WM_DRAWCLIPBOARD, проверяем, в буфере ли обмена строка, и если да, запускаем hex2dec на выполнение. Попутно я также определял, стоит показывать результаты преобразования из трея или нет. Похоже, кто-то из Microsoft внял слезным стенаниям разработчиков, и во второй версии фреймуорка они таки одарили нас "баллонами" на любой вкус.

1 using System;
2 using System.ComponentModel;
3 using System.Diagnostics;
4 using System.Drawing;
5 using System.Globalization;
6 using System.Runtime.InteropServices;
7 using System.Text.RegularExpressions;
8 using System.Windows.Forms;
9
10 namespace hex2dec
11 {
12 public partial class MainForm : Form
13 {
14 #region Fields
15
16 /// <summary>
17 /// Строковое представление числа, которое необходимо преобразовать.
18 /// </summary>
19 private TextBox inputTextBox = new TextBox();
20
21 /// <summary>
22 /// Дает команду на преобразование.
23 /// </summary>
24 private Button convertButton = new Button();
25
26 /// <summary>
27 /// Выводит результаты преобразования.
28 /// </summary>
29 private MyListBox outputListBox = new MyListBox();
30
31 /// <summary>
32 /// Отображает авторские права на приложение.
33 /// </summary>
34 private StatusBar statusBar = new StatusBar();
35
36 /// <summary>
37 /// Указатель на следующее окно в цепочке.
38 /// </summary>
39 private IntPtr hWndNextViewer;
40
41 /// <summary>
42 /// Определяет, нужно ли проверять содержимое буфера обмена.
43 /// Применяется для того, чтобы не проверять буфер во время
44 /// старта приложения.
45 /// </summary>
46 private bool shouldCheck = false;
47
48 /// <summary>
49 /// Содержит строку в буфере обмена.
50 /// </summary>
51 private string clipString = "";
52
53 /// <summary>
54 /// Содержит вывод консоли.
55 /// </summary>
56 private string outputBufferString = "";
57
58 /// <summary>
59 /// Возвращает полное имя типа <c>string</c>.
60 /// </summary>
61 private readonly string format = typeof(string).FullName;
62
63 /*
64 * Переменные, необходимые для корректного позиционирования
65 * элементов управления на форме.
66 */
67 private const int OFFSET_X = 5;
68 private const int OFFSET_Y = 5;
69
70 /*
71 * Стили окна.
72 */
73
74 /// <summary>
75 /// Создает изначально видимое окно.
76 /// </summary>
77 private const int WS_VISIBLE = 0x10000000;
78
79 /*
80 * Сообщения, которые Windows посылает в оконную процедуру.
81 */
82
83 /// <summary>
84 /// Извещает окно о том, что содержимое буфера изменилось.
85 /// </summary>
86 private const int WM_DRAWCLIPBOARD = 0x0308;
87
88 /// <summary>
89 /// Это сообщение посылается в оконную процедуру перед тем, как окно станет
90 /// видимым.
91 /// </summary>
92 private const int WM_CREATE = 0x0001;
93
94 /// <summary>
95 /// Это сообщение посылается в оконную процедуру, когда другое окно
96 /// вышло из цепочки.
97 /// </summary>
98 private const int WM_CHANGECBCHAIN = 0x030D;
99
100 /// <summary>
101 /// Это сообщение посылается в оконную процедуру в момент уничтожения, когда
102 /// оно уже скрылось с экрана.
103 /// </summary>
104 private const int WM_DESTROY = 0x0002;
105
106 /// <summary>
107 /// Добавляет указанное окно в цепочку.
108 /// </summary>
109 /// <param name="hWnd">Указатель на окно, которое необходимо добавить.</param>
110 /// <returns>Возвращает указатель на следующее окно в цепочке.</returns>
111 [DllImport("user32.dll")]
112 private static extern IntPtr SetClipboardViewer(IntPtr hWnd);
113
114 /// <summary>
115 /// Посылает указанное сообщение окну или окнам.
116 /// </summary>
117 /// <param name="hWnd">Указатель на окно, которому необходимо послать сообщение.</param>
118 /// <param name="msg">Идентификатор сообщения.</param>
119 /// <param name="wParam">Первый параметр.</param>
120 /// <param name="lParam">Второй параметр.</param>
121 /// <returns>Результат работы функции. Зависит от посланного сообщения.</returns>
122 [DllImport("user32.dll")]
123 private static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
124
125 /// <summary>
126 /// Удаляет заданное окно из цепочки.
127 /// </summary>
128 /// <param name="hWndRemove">Указатель на окно, которое следует удалить.</param>
129 /// <param name="hWndNewNext">Указатель на следующее окно в цепочке.</param>
130 /// <returns>Возвращает <c>true</c>, если операция завершилась успешно, иначе — <c>false</c>.</returns>
131 [DllImport("user32.dll")]
132 private static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);
133
134 #endregion
135
136 #region Methods
137
138 /// <summary>
139 /// Переключает язык ввода на английский.
140 /// </summary>
141 private void setInputLanguage()
142 {
143 InputLanguage.CurrentInputLanguage = InputLanguage.FromCulture(new CultureInfo("en-US"));
144 }
145
146 /// <summary>
147 /// Интерфейс с внешним консольным приложением hex2dec.
148 /// </summary>
149 /// <param name="arguments">Аргументы, которые необходимо подать на вход вызываемого
150 /// консольного приложения.</param>
151 /// <param name="shouldBalloonPopup">Определяет, нужно ли показывать подсказку
152 /// после конвертации.</param>
153 protected void FrontEnd(string arguments, bool shouldBalloonPopup)
154 {
155 Process p = new Process();
156 p.StartInfo.Arguments = arguments;
157
158 /* Говорим, что не хотим отображать окно вызываемого приложения. */
159 p.StartInfo.CreateNoWindow = true;
160 p.StartInfo.FileName = "hex2dec.exe";
161
162 /* Перенаправляем вывод консоли. */
163 p.StartInfo.RedirectStandardOutput = true;
164 p.StartInfo.UseShellExecute = false;
165
166 /* Запускаем внешнее приложение. */
167 p.Start();
168
169 /* Ждем его закрытия, чтобы узнать, успешно ли завершилась операция. */
170 p.WaitForExit();
171
172 /* Если операция завершилась успешно... */
173 if (p.ExitCode != -1) {
174 /* ...пропускаем пять строчек вывода в консоль, чтобы добраться до результата. */
175 for (int i = 0; i < 5; i++) {
176 p.StandardOutput.ReadLine();
177 }
178
179 /* Получаем результат конвертирования. */
180 this.outputBufferString = p.StandardOutput.ReadLine();
181
182 /* Добавляем вывод в лог. */
183 this.outputListBox.Items.Add(outputBufferString);
184
185 /*
186 * Устанавливаем текст у иконки в соответствии с
187 * результатом последнего успешного конвертирования.
188 */
189 this.notifyIcon.Text = outputBufferString;
190
191 /* Возможно, нужно отобразить результат конвертирования в виде всплывающей подсказки. */
192 if (shouldBalloonPopup) {
193 this.notifyIcon.ShowBalloonTip(200, "hex2dec", this.outputBufferString, ToolTipIcon.Info);
194 }
195 }
196
197 /*
198 * Выделяем текст, чтобы можно было вводить новый не удаляя старый.
199 */
200
201 this.inputTextBox.SelectionStart = 0;
202 this.inputTextBox.SelectionLength = this.inputTextBox.Text.Length;
203 this.inputTextBox.Focus();
204 }
205
206 protected override void WndProc(ref Message m)
207 {
208 switch (m.Msg) {
209 case WM_CREATE:
210 /* Встаем в цепочку для получения уведомлений об изменениях в буфере обмена. */
211 hWndNextViewer = SetClipboardViewer(base.Handle);
212 break;
213 case WM_CHANGECBCHAIN:
214 /* Одно из окон вышло из цепочки. Восстанавливаем ее. */
215 if ((IntPtr)m.WParam == this.hWndNextViewer) {
216 hWndNextViewer = (IntPtr)m.LParam;
217 }
218 else if (this.hWndNextViewer != null) {
219 SendMessage(this.hWndNextViewer, m.Msg, m.WParam, m.LParam);
220 }
221 break;
222 case WM_DESTROY:
223 /* Перед уничтожением окна выходим из цепочки. */
224 ChangeClipboardChain(base.Handle, this.hWndNextViewer);
225 break;
226 case WM_DRAWCLIPBOARD:
227 /* Отслеживаем изменения в буфере обмена. */
228 if (shouldCheck) { /* Стоит ли проверять содержимое буфера обмена. */
229 IDataObject dataObject = Clipboard.GetDataObject();
230 if (dataObject.GetDataPresent(this.format)) {
231 /* Получаем данные из буфера обмена только в том случае, если это строка. */
232 this.clipString = dataObject.GetData(DataFormats.StringFormat) as string;
233
234 /* Вызываем hex2dec. */
235 this.FrontEnd(this.clipString, true);
236 }
237 }
238 else {
239 this.shouldCheck = true;
240 }
241
242 /* Посылаем сообщение об изменении содержимого буфера обмена дальше по цепочке. */
243 SendMessage(this.hWndNextViewer, m.Msg, m.WParam, m.LParam);
244 break;
245 }
246
247 base.WndProc(ref m);
248 }
249
250 #endregion
251
252 #region Event Handlers
253
254 private void convertButton_Click(object sender, EventArgs e)
255 {
256 this.FrontEnd(this.inputTextBox.Text, false);
257 }
258
259 private void inputTextBox_TextChanged(object sender, EventArgs e)
260 {
261 this.setInputLanguage();
262 }
263
264 private void notifyIcon_DoubleClick(object sender, EventArgs e)
265 {
266 this.WindowState = FormWindowState.Normal;
267 this.Activate();
268 }
269
270 #endregion
271
272 #region Properties
273
274 protected override CreateParams CreateParams
275 {
276 get
277 {
278 CreateParams param = base.CreateParams;
279
280 /*
281 * Это т.н. хак. Необходимо для того, чтобы при загрузке формы с ShowInTaskBar = false
282 * кнопка последнего активного приложения не оставалась вдавленной.
283 */
284 param.Style |= WS_VISIBLE;
285
286 return param;
287 }
288 }
289
290 #endregion
291
292 #region Constructors
293
294 public MainForm()
295 {
296 /* MainForm */
297 InitializeComponent();
298 this.Size = new Size(280, 290);
299
300 /* Button */
301 this.convertButton.Click += new EventHandler(convertButton_Click);
302 this.convertButton.Location = new Point(
303 this.ClientRectangle.Right — (this.convertButton.Width + OFFSET_X),
304 this.ClientRectangle.Top + OFFSET_Y
305 );
306 this.convertButton.Text = "&Convert";
307 this.AcceptButton = this.convertButton;
308
309 /* TextBox */
310 this.inputTextBox.Location = new Point(
311 this.ClientRectangle.Left + OFFSET_X,
312 this.ClientRectangle.Top + OFFSET_Y
313 );
314 this.inputTextBox.TextChanged += new EventHandler(inputTextBox_TextChanged);
315 this.inputTextBox.Width = this.ClientRectangle.Width — (OFFSET_X * 3 + this.convertButton.Width);
316
317 /* MyListBox */
318 this.outputListBox.Location = new Point(
319 this.ClientRectangle.Left + OFFSET_X,
320 this.ClientRectangle.Top + (OFFSET_Y * 2 + this.inputTextBox.Height)
321 );
322 this.outputListBox.Size = new Size(
323 this.inputTextBox.Width,
324 this.ClientRectangle.Height — (this.statusBar.Height + this.inputTextBox.Height + OFFSET_Y * 3)
325 );
326
327 /* StatusBar */
328 this.statusBar.SizingGrip = false;
329 this.statusBar.Text = " 2005, <NAME>";
330
331 /* Processing controls */
332 this.Controls.AddRange(new Control[] {
333 this.inputTextBox,
334 this.convertButton,
335 this.outputListBox,
336 this.statusBar
337 });
338 }
339
340 #endregion
341 }
342 }

3. Заключение

На сегодня все. Совместными усилиями мы с вами реализовали самый ответственный участок бизнес-логики нашего детища. Остальное — уже не более, чем марафет. Можно, например, приказать программе все время сидеть в трее и запускаться вместе с Windows, добавить контекстное меню, диалоговое окно настроек и т.п. Сам же я в лучших традициях Linux написал конфигурационный файл, откуда и считываю все необходимые мне данные, т.к. графический интерфейс получился "тесным", и я не хотел его загромождать дополнительными кнопочками, менюшечками, рюшечками... Но в первую очередь замечательно то, что мы можем подогнать собственноручно написанный софт под себя, делая его поистине уникальным. Вот уж где открываются бескрайние просторы для самовыражения!

2005, Алексей Нестеров, eisernWolf@tut.by


Компьютерная газета. Статья была опубликована в номере 36 за 2005 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета