пятница, 4 сентября 2009 г.

Глобальный хук на мышку в .NET

Понадобилось мне как-то реализовать возможность авто-логаута для проекта с системой аутентификации. То есть, после простоя проекта без использования, он должен был скрываться и запрашивать пароль заново. Проект основывался на Windows Forms. Поискав в Интернете информацию, мне не понадобилось много времени, чтобы приладить хук на мышку к моему приложению.

Вот код, который понадобился мне для реализации этой возможности.

static class Program
{
private static FormMain m_formMain = null;
private static LowLevelMouseProc _proc = HookCallback;
private static IntPtr _hookID = IntPtr.Zero;

private static IntPtr SetHook(LowLevelMouseProc proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_MOUSE_LL, proc,
GetModuleHandle(curModule.ModuleName), 0);
}
}

private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);

private static IntPtr HookCallback(
int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && MouseMessages.WM_MOUSEMOVE == (MouseMessages)wParam)
{
if (m_formMain != null)
m_formMain.ResetLogoutTimer();
}
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}

private const int WH_MOUSE_LL = 14;

private enum MouseMessages
{
WM_LBUTTONDOWN = 0x0201,
WM_LBUTTONUP = 0x0202,
WM_MOUSEMOVE = 0x0200,
WM_MOUSEWHEEL = 0x020A,
WM_RBUTTONDOWN = 0x0204,
WM_RBUTTONUP = 0x0205
}

[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int x;
public int y;
}

[StructLayout(LayoutKind.Sequential)]
private struct MSLLHOOKSTRUCT
{
public POINT pt;
public uint mouseData;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook,
LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
IntPtr wParam, IntPtr lParam);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);

///
/// The main entry point for the application.
///

[STAThread]
static void Main()
{
Application.EnableVisualStyles();
_hookID = SetHook(_proc);
m_formMain = new FormMain();
Application.Run(m_formMain);
UnhookWindowsHookEx(_hookID);
}
}


FormMain - класс главной формы моего приложения.

Функция-callback - HookCallback позволяет нам фиксировать различные действия мышки. Таким образом, чтобы сбросить таймер авто-логаута необходимо отловить сообщение WM_MOUSEMOVE и вызвать соответствующие функции. Каждый раз, когда мышка двигается на экране (даже если приложение свернуто), таймер начинает заново с нуля отсчитывать секунды бездействия пользователя.

понедельник, 31 августа 2009 г.

Скачивание текста HTML страницы с удаленного сервера

Довольно тривиальная задача, с которой могут многие столкнуться. Но ее решение не очевидно. Мне это понадобилось, когда я разрабатывал AldoranSpy. Суть была в том, что необходимо было получить содержимое HTML-страницы с удаленного сервера. Пытаясь использовать компонент WebBrowser, у меня возникали трудности различного характера. К тому же было понятно, что WebBrowser - компонент визуальный (а ведь нам не требуется ничего отображать) и держать его невидимым было уж как-то совсем неправильно.

Довольно долго искал, как правильно было использовать WebBrowser, в то время как его использование совсем не требовалось. Ответ крылся в использовании класса WebClient.

using System.Net;

...

string strFileContent;
WebClient Client = new WebClient();
Stream strm = Client.OpenRead("http://www.aldoran.ru/?page=stat");
StreamReader sr = new StreamReader(strm);
strFileContent = sr.ReadToEnd();
strm.Close();


В итоге, в переменной strFileContent получаем содержимое HTML-страницы. Но это далеко не единственное применение WebClient. Также можно качать и картинки:

using System.Net;

...

Bitmap m_bmpFileContent;
WebClient Client = new WebClient();
Stream strm = Client.OpenRead("http://www.aldoran.ru/userbars/" + m_strUserName + ".png");
bmpFileContent = new Bitmap(strm);
strm.Close();


Вполне естественно, что картинки - тоже не последнее, что может делать WebClient. К примеру, с помощью него можно загружать файла на удаленный ресурс. Более подробно вы все это можете прочитать на странице MSDN.

воскресенье, 30 августа 2009 г.

Inno Setup и .NET Framework 2.0

Довелось мне делать проект, который должен был быть очень юзабелен (то есть для простых людей). Платформой проекта была выбрана .NET, а значит, это накладывало некоторые ограничения на его использование на старых операционных системах (типа Windows XP), в которых по умолчанию не установлена эта платформа.

Таким образом, возникла задача - инсталляционный скрипт должен был быть "интеллектуальным", то есть уметь:

  • Определять, требуется ли установка платформы на этом компьютере;
  • И устанавливать эту платформу, естественно. :)

Существует множество различных компиляторов инсталляторов, среди них, к примеру:


Список можно продолжать очень долго, таких компиляторов огромное количество. Я лишь перечислил наиболее популярные среди них.

Мой выбор пал на Inno Setup. Потому как мне кажется этот компилятор наиболее простым в применении и в то же время очень мощным (к примеру, он поддерживает скрипты на языке Pascal и сжатие LZMA). К тому же, тело установщика также очень мало.

Итак, начал я с того, что скачал платформу .NET в виде Redistributable Package. Скачать ее можно, например, отсюда:
http://www.microsoft.com/downloads/details.aspx?FamilyID=0856EACB-4362-4B0D-8EDD-AAB15C5E04F5&displaylang=en

Затем, распаковал с помощью 7-zip скачанный EXE-файл и получил набор установочных файлов, среди которых есть install.exe (устанавливает, собственно, саму платформу).

Параметры командной строки для установки платформы в тихом режиме (или восстановления, если она уже есть на целевой машине): "/q" (без кавычек).

Как я уже сказал, есть один нюанс: если на целевой машине уже установлена .NET 2.0, то установка в тихом режиме представляет собой восстановление. Это занимает много времени при установке, что неприемлемо. Причем восстановление, как правило, совершенно не нужно. Других полезных параметров командной строки для выбора режима установки у install.exe нет. Это ставит перед нами еще один барьер: требуется знать, установлена ли платформа на целевой машине.

На помощь приходит поддержка Pascal-скриптов. Стоит отметить, что эти скрипты не являются компилируемыми и исполняются интерпретатором в реальном времени во время работы инсталлятора. Недолго изучая этот скриптовый паскаль, у меня родилась следующая секция Code:

[Code]
function DotNet20NotInstalled(): Boolean;
var
Installed: Cardinal;
begin
if RegKeyExists(HKEY_LOCAL_MACHINE, 'Software\Microsoft\NET Framework Setup\NDP\v2.0.50727') then
Result := False
else
Result := True;
end;


Теперь у нас есть функция, проверяющая, есть ли на целевой машине платформа .NET версии 2.0. Отлично! Осталось добавить в секцию Files установочные файлы этой платформы:

[Files]
Source: "dotnetfx\*"; DestDir: "{tmp}\dotnetfx"; Flags: ignoreversion recursesubdirs createallsubdirs; Check: DotNet20NotInstalled;


Source параметр содержит путь к установочным файлам .NET. DestDir указывает на временную папку, которая судя по документации выглядит примерно так C:\WINDOWS\TEMP\IS-xxxxx.tmp и создается при работе нашего установщика и деинсталлятора.

Обратите внимение на последний параметр Check. Он позволяет нам избежать распаковки установочных файлов платформы, если это не требуется.

И последнее, запуск установки платформы во время работы установщика. Добавляем соответствующий пункт в секцию Run:

[Run]
Filename: "{tmp}\dotnetfx\install.exe"; Parameters: "/q"; StatusMsg: "Установка Microsoft .NET Framework 2.0... Пожалуйста, дождитесь завершения."; Check: DotNet20NotInstalled;


Думаю, в этой секции все параметры интуитивно понятны и не должны вызвать у вас вопросов.

Итогом компиляции такого скрипта будет наш "умный" установщик с необходимыми требованиями.

Надеюсь, что помог вам сэкономить время на создании установщика, способного самостоятельно следить за средой в которой он запущен и адекватно реагировать на нее.

среда, 5 августа 2009 г.

Шрифты диалога по умолчанию

Еще давно, разрабатывая приложения на С++ и WTL я сталкивался с задачей, в которой было плохо понятно, какой шрифт необходимо использовать для отображения текста на контролах в ОС Windows. Каждая новая версия Windows сопровождалась новыми шрифтами, которые использовались в системных диалогах по умолчанию. К примеру, в Windows 98 по умолчанию диалоговый шрифт был "MS Sans Serif", затем, с выходом Windows XP - "Tahoma", Windows Vista и Windows 7 - "Segoe UI". С каждой новой версией шрифты становились более приятными глазу (это уже лично мое мнение), но нельзя было точно понять, какой же шрифт система использует по умолчанию для отображения диалогов. Кстати, когда я говорю о диалогах, проще всего представить себе обычное сообщение об ошибке.
Оказалось, .NET 2.0 поддерживает определение основного шрифта системы. Сама же .NET по умолчанию использует "Microsoft Sans Serif", что не очень красиво смотрится в большинстве форм. "Microsoft Sans Serif" является усовершенствованным "MS Sans Serif" и, в отличие от последнего, поддерживает ClearType сглаживание.
Функцию изменения шрифта на системный можно написать примерно следующим образом:
public static void SetDialogFont(Control.ControlCollection controls)
{
foreach (Control c in controls)
{
Font old = c.Font;
c.Font = new Font(SystemFonts.MessageBoxFont.FontFamily.Name, old.Size, old.Style);
}
}
Все элементарно... Вы, конечно, можете попробовать использовать размер SystemFonts.MessageBoxFont.FontFamily.Size для контролов, но тогда никто не даст гарантии, что ваши контролы не "поплывут" после изменения размера шрифта, так как .NET контролы изменяют свой размер и подстраиваются под размер шрифта динамически.
Вызвать функцию SetDialogFont целесообразнее всего в конструкторе формы, сразу после функции InitializeComponent() (в которой, собственно, и создаются все объекты контролов).
public FormMain()
{
InitializeComponent();
SetDialogFont(Controls);

...
}
Ну вот, на этом все... Взглянем на результаты в Windows 7.
До:
После:
Надеюсь, это вам поможет в создании более симпатичных форм для ваших приложений.

вторник, 4 августа 2009 г.

SQLite в .NET

Однажды понадобилось мне создать программку-мигратор базы данных из текстового XML в SQLite для проекта vanac. Дело в том, что XML меня перестал устраивать ввиду своих очень жадных потребностей в смысле места. К тому же, XML на лету было не так-то легко менять. Требовалась динамичность изменения данных.

Таким образом, для локального проекта наиболее оптимальным решением было выбрать базу данных SQLite.

Искать в Интернете информацию долго не пришлось. Сразу же натолкнулся на движок, разработанный специально для такого случая:
http://sqlite.phxsoftware.com/

Скачав и установив его, я открыл Visual Studio и обнаружил в списке возможных для добавления References: System.Data.SQLite. Для тех, кто не знает, как добавлять зависимости для приложений приведу парочку скриншотов.

Давим в Solution Explorer правой кнопкой.



Затем выбираем в списке System.Data.SQLite и давим OK.



Зависимость добавлена.

Вот три основные функции, которые обеспечат вам понимание принципов работы с пространством имен System.Data.SQLite.
GetDataTable(string sql) выполняет запрос sql и возвращает результат его выполнения в структуре данных DataTable.
ExecuteNonQuery(string sql) используется в основном, когда необходимо выполнить операцию над данными (например, UPDATE или INSERT INTO). Возвращаемое значение - количество измененных / добавленных рядов данных.
ExecuteScalar(string sql) используется, когда ожидается получить одно единственное значение (например, количество юзеров в таблице users).


public static DataTable GetDataTable(string sql)
{
SQLiteConnection sqliteConnection = new SQLiteConnection("Data Source=Base.db");
sqliteConnection.Open();
SQLiteCommand sqliteCommand = new SQLiteCommand(sqliteConnection);
DataTable dt = new DataTable();
try
{
sqliteCommand.CommandText = sql;
SQLiteDataReader sqliteReader = sqliteCommand.ExecuteReader();
dt.Load(sqliteReader);
sqliteReader.Close();
}
catch
{
// Закрывать соединение нужно в любом случае
sqliteConnection.Close();
}
sqliteConnection.Close();
return dt;
}

public static int ExecuteNonQuery(string sql)
{
SQLiteConnection sqliteConnection = new SQLiteConnection("Data Source=Base.db");
sqliteConnection.Open();
SQLiteCommand sqliteCommand = new SQLiteCommand(sqliteConnection);
sqliteCommand.CommandText = sql;
int rowsUpdated = sqliteCommand.ExecuteNonQuery();
sqliteConnection.Close();
return rowsUpdated;
}

public static string ExecuteScalar(string sql)
{
SQLiteConnection sqliteConnection = new SQLiteConnection("Data Source=Base.db");
sqliteConnection.Open();
SQLiteCommand sqliteCommand = new SQLiteCommand(sqliteConnection);
sqliteCommand.CommandText = sql;
object value = sqliteCommand.ExecuteScalar();
sqliteConnection.Close();
if (value != null)
{
return value.ToString();
}
return "";
}


Если при использовании SQLite у вас возникла проблема с кодировкой русских символов, попробуйте в базе использовать тип данных NVARCHAR вместо VARCHAR.

понедельник, 3 августа 2009 г.

Отображение рамок фокуса

В одном проекте для платформы .NET мне понадобилось принудительно отобразить рамки фокуса (они бывают у контролов Button, RadioBox, CheckBox). При разработке приложений, которые в большинстве своем управляются с клавиатуры иногда бывает просто необходимо за кратчайшее время знать, на каком контроле находится текущий фокус. У кнопок, например, фокус может отображаться в двух состояниях:

На самом деле, Windows настроена так, чтобы автоматом включать режим отображения рамок по клавише TAB. Но у меня возникла проблема. Переходы надо было осуществлять по кнопке Enter и в форме, на которой куча контролов, было легко заблудиться без рамок фокуса.
Как оказалось, без использования Windows API обойтись было нельзя. Поэтому после некоторого поиска в гугле, я натолкнулся на описание сообщения WM_UPDATEUISTATE, которое, как раз и отвечает за состояние контролов в окне. После родился следующий код:
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
public static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

public static IntPtr MakeLong(short lowPart, short highPart)
{
return (IntPtr)(((ushort)lowPart) | (uint)(highPart << 16));
}

public static void ShowFocusCues(IntPtr Handle)
{
const uint WM_UPDATEUISTATE = 0x0128;

//short UIS_SET = 1;
short UIS_CLEAR = 2;
//short UIS_INITIALIZE = 3;

short UISF_HIDEFOCUS = 0x1;
//short UISF_HIDEACCEL = 0x2;
//short UISF_ACTIVE = 0x4;

Common.SendMessage(Handle, WM_UPDATEUISTATE, MakeLong(UIS_CLEAR, UISF_HIDEFOCUS), (IntPtr)0);
}

Используя функцию ShowFocusCues(this.Handle) с параметром, указывающим на хендл активного окна, мы можем сменить режим отображения контролов на тот, что и требуется. То есть, принудительно отобразить рамки фокуса. Хендл окна описан в классе Form.

Также, если понадобится принудительно спрятать рамки фокуса, то можно использовать немного иную реализацию отправки сообщения WM_UPDATEUISTATE:

public static void HideFocusCues(IntPtr Handle)
{
const uint WM_UPDATEUISTATE = 0x0128;

short UIS_SET = 1;
//short UIS_CLEAR = 2;
//short UIS_INITIALIZE = 3;

short UISF_HIDEFOCUS = 0x1;
//short UISF_HIDEACCEL = 0x2;
//short UISF_ACTIVE = 0x4;

SendMessage(Handle, WM_UPDATEUISTATE, MakeLong(UIS_SET, UISF_HIDEFOCUS), (IntPtr)0);
}