netlib.narod.ru | < Назад | Оглавление | Далее > |
Вообще написать код, использующий отслеживание мыши и работающий в 99,5% случаев, довольно просто (см. ниже). По структуре эта программа очень напоминает MouseConnect, но выполняет более привычное действие — позволяет нарисовать прямоугольник путем перемещения мыши с нажатой кнопкой.
BlockOut.cs
//----------------------------------------- // BlockOut.cs (C) 2001 by Charles Petzold //----------------------------------------- using System; using System.Drawing; using System.Windows.Forms; class BlockOut: Form { bool bBlocking, bValidBox; Point ptBeg, ptEnd; Rectangle rectBox; public static void Main() { Application.Run(new BlockOut()); } public BlockOut() { Text = "Blockout Rectangle with Mouse"; BackColor = SystemColors.Window; ForeColor = SystemColors.WindowText; } protected override void OnMouseDown(MouseEventArgs mea) { if (mea.Button == MouseButtons.Left) { ptBeg = ptEnd = new Point(mea.X, mea.Y); Graphics grfx = CreateGraphics(); grfx.DrawRectangle(new Pen(ForeColor), Rect(ptBeg, ptEnd)); grfx.Dispose(); bBlocking = true; } } protected override void OnMouseMove(MouseEventArgs mea) { if (bBlocking) { Graphics grfx = CreateGraphics(); grfx.DrawRectangle(new Pen(BackColor), Rect(ptBeg, ptEnd)); ptEnd = new Point(mea.X, mea.Y); grfx.DrawRectangle(new Pen(ForeColor), Rect(ptBeg, ptEnd)); grfx.Dispose(); Invalidate(); } } protected override void OnMouseUp(MouseEventArgs mea) { if (bBlocking && mea.Button == MouseButtons.Left) { Graphics grfx = CreateGraphics(); rectBox = Rect(ptBeg, new Point(mea.X, mea.Y)); grfx.DrawRectangle(new Pen(ForeColor), rectBox); grfx.Dispose(); bBlocking = false; bValidBox = true; Invalidate(); } } protected override void OnPaint(PaintEventArgs pea) { Graphics grfx = pea.Graphics; if (bValidBox) grfx.FillRectangle(new SolidBrush(ForeColor), rectBox); if (bBlocking) grfx.DrawRectangle(new Pen(ForeColor), Rect(ptBeg, ptEnd)); } Rectangle Rect(Point ptBeg, Point ptEnd) { return new Rectangle(Math.Min(ptBeg.X, ptEnd.X), Math.Min(ptBeg.Y, ptEnd.Y), Math.Abs(ptEnd.X - ptBeg.X), Math.Abs(ptEnd.Y - ptBeg.Y)); } }
Чтобы работать с этой программой, нажмите левую кнопку, передвиньте мышь и отпустите кнопку. При перемещении рисуется прямоугольный контур. Когда вы отпускаете кнопку, программа заливает нарисованный контур. При желании можно нарисовать новый прямоугольник, который заменит старый.
BlockOut использует две переменных bool, хранимых как поля: bBlocking и bValidBox. Переменная bBlocking показывает, что пользователь рисует прямоугольник. При исполнении метода OnMouseDown она равна true, а при исполнении OnMouseUp — false. Метод OnMouseMove проверяет значение этой переменной, чтобы определить, что ему делать. Если bBlocking равна true, OnMouseMove стирает контур предыдущего прямоугольника, повторно прорисовывая его цветом фона, и рисует контур нового прямоугольника цветом изображения. Когда кнопка отпущена, OnMouseUp присваивает переменной bBlocking значение false, a bValidBox — true. Последняя переменная позволяет методу OnPaint нарисовать залитый прямоугольник.
Я привык использовать в OnMouseMove методику рисования по принципу «исключающее ИЛИ» (eXclusive OR или XOR), XOR-рисование не просто рисует цветные пикселы на экране, а обращает цвета существующих пикселов. Линия, нарисованная таким способом на черном фоне, выглядит белой, а на голубом фоне — красной. Преимущество этой методики в том, что вторая XOR-линия, заданная теми же координатами, стирает первую.
Однако GDI+ не поддерживает XOR-рисование, поэтому я вынужден стирать предыдущий прямоугольник, используя цвет фона и метод OnMouseMove. При рисовании нового прямоугольника поверх существующего залитого прямоугольника появляются несколько неприятных артефактов, от которых надо избавиться. Поэтому обработка OnMouseMove заканчивается вызовом Invalidate, генерирующим событие Paint. Строго говоря, вызывать метод Invalidate не обязательно, но если вы удалите этот вызов, то поймете, почему мне пришлось включить его в программу. В случае XOR-рисования необходимость в вызове Invalidate попросту отпала бы.
Конечно, отсутствие XOR-рисования — недостаток GDI+, но и сама программа BlockOut не без изъяна.
Поэкспериментировав с BlockOut. вы заметите, что чаще всего она работает замечательно. Поскольку при нажатии кнопки мыши начинается ее захват, то при выходе курсора за пределы клиентской области программа продолжает вызывать метод OnMouseMove. А если отпустить кнопку мыши за пределами клиентской области, будет вызван метод OnMouseUp.
А теперь попробуйте сделать так: во время рисования прямоугольника, не отпуская левую кнопку, нажмите и отпустите правую. После отпускания правой кнопки форма теряет захват мыши. Теперь программа будет реагировать на движение мыши, только когда ее курсор находится в пределах клиентской области. Теперь выведите курсор за пределы клиентской области и отпустите левую кнопку. Форма не получает вызов OnMouseUp, так как захват мыши больше не выполняется. А сейчас, не нажимая кнопок, верните мышь в клиентскую область. Программа реагирует на движение мыши, хотя ни одна из ее кнопок не нажата! Ясно, что это совершенно нежелательно.
Можно предложить несколько исправлений, которые помогут решить эту проблему.
Прекращать захват мыши при отпускании любой кнопки. Этот подход более точно копирует обстоятельства, при которых захват мыши теряется.
В обработку OnMouseMove включить проверку постоянного нажатия левой кнопки. Если форма потеряла захват мыши, лучше, чтобы программа прекратила рисование контура прямоугольника, а не продолжала рисовать с отпущенной кнопкой мыши.
При нажатии клавиши Esc прекращать захват мыши.
Вот улучшенная версия программы BlockOut.
BetterBlockOut.cs
//----------------------------------------------- // BetterBlockOut.cs (C) 2001 by Charles Petzold //----------------------------------------------- using System; using System.Drawing; using System.Windows.Forms; class BetterBlockOut: Form { bool bBlocking, bValidBox; Point ptBeg, ptEnd; Rectangle rectBox; public static void Main() { Application.Run(new BetterBlockOut()); } public BetterBlockOut() { Text = "Better Blockout"; BackColor = SystemColors.Window; ForeColor = SystemColors.WindowText; } protected override void OnMouseDown(MouseEventArgs mea) { if (mea.Button == MouseButtons.Left) { ptBeg = ptEnd = new Point(mea.X, mea.Y); Graphics grfx = CreateGraphics(); grfx.DrawRectangle(new Pen(ForeColor), Rect(ptBeg, ptEnd)); grfx.Dispose(); bBlocking = true; } } protected override void OnMouseMove(MouseEventArgs mea) { if (bBlocking && (mea.Button & MouseButtons.Left) != 0) { Graphics grfx = CreateGraphics(); grfx.DrawRectangle(new Pen(BackColor), Rect(ptBeg, ptEnd)); ptEnd = new Point(mea.X, mea.Y); grfx.DrawRectangle(new Pen(ForeColor), Rect(ptBeg, ptEnd)); grfx.Dispose(); Invalidate(); } } protected override void OnMouseUp(MouseEventArgs mea) { if (bBlocking) { Graphics grfx = CreateGraphics(); rectBox = Rect(ptBeg, new Point(mea.X, mea.Y)); grfx.DrawRectangle(new Pen(ForeColor), rectBox); grfx.Dispose(); bBlocking = false; bValidBox = true; Invalidate(); } } protected override void OnKeyPress(KeyPressEventArgs kpea) { if (bBlocking && kpea.KeyChar == '\x001B') // Escape { Graphics grfx = CreateGraphics(); grfx.DrawRectangle(new Pen(BackColor), Rect(ptBeg, ptEnd)); grfx.Dispose(); bBlocking = false; Invalidate(); } } protected override void OnPaint(PaintEventArgs pea) { Graphics grfx = pea.Graphics; if (bValidBox) grfx.FillRectangle(new SolidBrush(ForeColor), rectBox); if (bBlocking) grfx.DrawRectangle(new Pen(ForeColor), Rect(ptBeg, ptEnd)); } Rectangle Rect(Point ptBeg, Point ptEnd) { return new Rectangle(Math.Min(ptBeg.X, ptEnd.X), Math.Min(ptBeg.Y, ptEnd.Y), Math.Abs(ptEnd.X - ptBeg.X), Math.Abs(ptEnd.Y - ptBeg.Y)); } }
В некоторых ситуациях эта программа все равно теряет захват мыши, не подозревая об этом. Если посреди операции с захватом вы нажмете Ctrl+Esc, чтобы вызвать меню Start, или Alt+Tab для переключения на другую программу, BlockOut и BetterBlockOut потеряют захват мыши, даже не узнав об этом. Потеря захвата мыши вовсе не обязательно является следствием действий пользователя. Допустим, посреди операции с захватом мыши выводится сообщение о том, что в принтере кончилась бумага. В этом случае программа также потеряет захват мыши, так как окно сообщения должно реагировать на действия мыши.
Не правда ли, было бы очень кстати событие, сообщающее форме о потере захвата мыши? Если б мы писали программу Win32, можно было бы перехватить сообщение WM_CAPTURECHANGED. Это сообщение генерируется каждый раз, когда окно теряет захват мыши штатным (при отпускании кнопки мыши) или нештатным образом. А можно ли реализовать в программе Windows Forms обработчик для этого сообщения?
Да, и для этого понадобится класс NativeWindow. Вот программа, демонстрирующая реализацию метода OnLostCapture с помощью NativeWindow в классе, производном от Form.
CaptureLoss.cs
//-------------------------------------------- // CaptureLoss.cs (C) 2001 by Charles Petzold //-------------------------------------------- using System; using System.Drawing; using System.Windows.Forms; class CaptureLoss: Form { public static void Main() { Application.Run(new CaptureLoss()); } public CaptureLoss() { Text = "Capture Loss"; // Подключить объект NativeWindow CaptureLossWindow win = new CaptureLossWindow(); win.form = this; win.AssignHandle(Handle); } protected override void OnMouseDown(MouseEventArgs mea) { Invalidate(); } public void OnLostCapture() { Invalidate(); } protected override void OnPaint(PaintEventArgs pea) { Graphics grfx = pea.Graphics; if (Capture) grfx.FillRectangle(Brushes.Red, ClientRectangle); else grfx.FillRectangle(Brushes.Gray, ClientRectangle); } } class CaptureLossWindow: NativeWindow { public CaptureLoss form; protected override void WndProc(ref Message message) { if (message.Msg == 533) // WM_CAPTURECHANGED form.OnLostCapture(); base.WndProc(ref message); } }
Здесь используются два класса: CaptureLoss, производный от Form, и CaptureLossWindow, производный от NativeWindow. К CaptureLossWindow я добавил поле, куда заносится объект типа CaptureLoss. CaptureLossWindow также переопределяет метод WndProc (процедуру окна) класса NativeWindow. Бее программисты API Win32 считают WndProc процедурой первостепенной важности для любой прикладной программы Windows, обрабатывающей сообщения, адресованные созданному программой окну.
В своем конструкторе CaptureLoss создает объект типа CaptureLossWindow. В поле form (которое я добавил к этому классу) заносится форма, которая создается программой. Конструктор также вызывает метод AssignHandle, реализованный в классе NativeWindow. Этот метод присваивает объект CaptureLossWindow дескриптору, связанному с формой, после чего CaptureLossWindow получает через свой метод WndProc все адресованные форме сообщения, хорошо знакомые программистам API Win32. Параметр Message процедуры WndProc — структура, определяемая в пространстве имен System.Windows.Forms и содержащая свойства, которые соответствуют всем параметрам-сообщениям Win32. Когда WndProc получает сообщение с идентификатором 533 (WM_CAPTURECHANGED), она вызывает метод OnLostCapture объекта CaptureLoss.
При вызове методов OnMouseDown и OnLostCapture класс CaptureLoss делает клиентскую область недействительной. Если свойство Capture равно true, метод OnPaint заливает клиентскую область красным, в противном случае — серым. Удобнее наблюдать захват мыши, установив флажок Show Window Contents While Dragging (Отображать содержимое окна при его перетаскивании) на вкладке Effects (Эффекты) в диалоговом окне Display Properties (Свойства: экран), вызываемом из Панели управления. Если взять окно за заголовок и частично перетащить его за пределы экрана, после чего вернуть назад, можно заметить следующее. Пока кнопка мыши нажата, часть окна, выходившая за пределы экрана, залита красным, а если отпустить кнопку, вся клиентская область вновь станет серой. Если растянуть окно мышью, то аналогичным образом новая часть окна сначала будет красной, а после отпускания кнопки мыши станет серой. Это происходит, поскольку свойство Capture равно true, даже если во время нажатия кнопки мыши курсор находится над заголовком или границей окна программы.
netlib.narod.ru | < Назад | Оглавление | Далее > |