Пишемо просту програму захоплення скріншотів

Існує безліч різних програм для захоплення зображень з екрану, їх редагувати прямо на екрані» і завантаження на різні сервіси. Це все добре, але більшість програм прив'язані до певних сервісів і не дають можливості завантажувати куди-небудь ще. В голові давно вже крутилася думка створити свій простенький сервіс завантаження картинок під свої потрібні. І я хочу поділитися історією розробки даної програми.

Не довго думаючи і маючи під рукою Visual Studio 2015 звичайно ж створив новий C# проект т. к. це дуже зручно і я вже робив раніше невеликі C# програми.

Завдання перша
Глобальний перехоплення натискання кнопок PrintScreen і Alt+PrintScreen. Щоб не винаходити велосипед, пару хвилин гугления і майже одразу знайшлося рішення. Суть полягає у використанні callback-функції LowLevelKeyboardProc і функції SetWindowsHookEx з WH_KEYBOARD_LL з user32.dll. З невеликою модифікацією під перехоплення двох комбінацій код заробив і успішно ловить натискання клавіш.

Код захоплення натискання клавіш
namespace ScreenShot_Grab
{
static class Program
{
private static MainForm WinForm;
/// <summary>
/// Головна точка входу для застосування.
/ / / < /summary>

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

private const int WH_KEYBOARD_LL = 13;
//private const int WH_KEYBOARD_LL = 13; 
private const int VK_F1 = 0x70;
private static LowLevelKeyboardProc _proc = HookCallback;
private static IntPtr _hookID = IntPtr.Zero;

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

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

private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
if (nCode >= 0) {
Keys number = (Keys)Marshal.ReadInt32(lParam);
//MessageBox.Show(number.ToString());
if (number == Keys.PrintScreen) {
if (wParam == (IntPtr)261 && Keys.Alt == Control.ModifierKeys && number == Keys.PrintScreen) {
// Alt+PrintScreen
} else if (wParam == (IntPtr)257 && number == Keys.PrintScreen) {
// PrintScreen
}
}
}

return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc 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);

}

}


Завдання друга
Власне захоплення скріншота при натисканні клавіш. Знову гугление і рішення знайдено. У цьому випадку використовуються функції GetForegroundWindow і GetWindowRect все з того ж user32.dll, а також штатна функція .NET Graphics.CopyFromScreen. Пару перевірок та код працює, але з однією проблемою — захоплює також межі вікна. До вирішення цього питання повернуся трохи пізніше.

Код захоплення скріншотів

class ScreenCapturer
{

public enum CaptureMode
{
Screen,
Window
}

[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();

[DllImport("user32.dll")]
private static extern IntPtr GetWindowRect(IntPtr hWnd, ref Rect rect);

[StructLayout(LayoutKind.Sequential)]
public struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}

public Bitmap Capture(CaptureMode screenCaptureMode = CaptureMode.Window)
{
Rectangle bounds;

if (screenCaptureMode == CaptureMode.Screen)
{
bounds = Screen.GetBounds(Point.Empty);
CursorPosition = Cursor.Position;
}
else
{
var handle = GetForegroundWindow();
var rect = new Rect();
GetWindowRect(handle, ref rect);

bounds = new Rectangle(rect.Left, rect.Top, rect.Right, rect.Bottom);
//CursorPosition = new Point(Cursor.Position.X - rect.Left, Cursor.Position.Y - rect.Top);
}

var result = new Bitmap(bounds.Width, bounds.Height);

using (var g = Graphics.FromImage(result))
{
g.CopyFromScreen(new Point(bounds.Left, bounds.Top), Point.Empty, bounds.Size);
}

return result;
}

public Point CursorPosition
{
get;
protected set;
}
}


Завдання третя
Збереження скріншота на комп'ютер, тут все дуже просто достатньо було використовувати функцію Bitmap.Save.


private void save_Click(object sender, EventArgs e)
{
if (lastres == null) { return; }
// генеруємо ім'я з допомогою base36
Int32 unixTimestamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
var FileName = base_convert(unixTimestamp.ToString(), 10, 36);
lastres.Save(spath + FileName);
}

Завдання четверта
Завантаження скріншота на сервер, тут начебто здається, що все просто, але це не зовсім так. Після невеликого роздуми прийшла в голову досить проста ідея — завантажувати скріншот за допомогою WebClient в бінарному форматі використовуючи заголовок «application/octet-stream» і функцію WebClient.UploadData, а на стороні сервера брати дані з допомогою file_get_contents(«php://input»). Власне так і вчинив, написав дуже простий php скрипт в пару рядків і прив'язав все це справа до програми. Підсумок — скріншоти зберігає і завантажує. Разом з цим треба було знайти простий алгоритм генерації коротких посилань, разом нагуглил дуже простий і елегантний спосіб полягає у використанні Base36, взявши за int unix час в секундах (linux epoch).


// переводимо bitmap у byte[]
private Byte[] BitmapToArray(Bitmap bitmap)
{
if (bitmap == null) return null;
using (MemoryStream stream = new MemoryStream()) {
bitmap.Save(stream, ImgFormat[Properties.Settings.Default.format]);
return stream.ToArray();
}
}

private void upload_Click(object sender, EventArgs e)
{
using (var client = new WebClient()) {
client.Headers.Add("Content-Type", "application/octet-stream");
try {
var response = client.UploadData(svurl, BitmapToArray(lastres);
var result = Encoding.UTF8.GetString(response);
if (result.StartsWith("http")) {
System.Diagnostics.Process.Start(result);
}
} catch { }
}
}

Приймає PHP-скрипт
<?php
$file = file_get_contents("php://input");
$id = base_convert(time(), 10, 36);
file_put_contents("img/".$id.".png",$file);
echo "http://".$_SERVER['SERVER_NAME']."/img/".$id.".png";
?>

" Редагування скріншотів
Далі захотілося також якось швидко редагувати скріншоти і завантажувати їх на сервер. Замість винаходу чергового редактора зображень народилася дуже проста ідея — зробити кнопку «редагувати», яка відкривала paint із захопленим скріншотом (останніми що зберіг на диск), а після редагування можна було спокійно завантажити цей файл на сервер.


private void edit_Click(object sender, EventArgs e)
{
if (lastres == null) return;
if (lastfile == "") save_Click(sender, e);
Process.(Start"mspaint.exe", "\"" + lastfile + "\"");
}

Налаштування
Також треба було десь вказувати url сайту і папку за замовчуванням куди зберігати скріншоти, в підсумку створив простеньку форму налаштувань де це можна було вказати. Ну і до того ж зробив кнопку «відкрити папку» щоб все було ще простіше і швидше з допомогою функції System.Diagnostics.Process.Start. Крім цього швидко навчив програму згортатися в трей.

Отже після всього цього був готовий перший робочий прототип, і виглядав він так:


попередній перегляд
Все начебто добре, але стало зрозуміло, чого не вистачає. А не вистачало кнопки предосмотра! Було дещо не зручно відкривати папку або натискати редагувати щоб тільки подивитися що ж захватилось з екрану перед відправкою. У підсумку швидко накидав форму предосмотра, була невелика проблема з відображенням повноекранного скріншота у формі (адже вона з рамками), рамки видаляти не хотілося (навіть не знаю чому), в результаті зробив скрол у формі і мене таке повністю влаштувало.


private void PreviewForm_Load(object sender, EventArgs e)
{
if (form1.lastfile!="") {
img.Image = Image.FromFile(form1.lastfile);
} else {
img.Image = form1.lastres;
}
ClientSize = new Size(img.Image.Width + 10, img.Image.Height + 10);
img.Width = img.Image.Width+10;
img.Height = img.Image.Height+10;
if (img.Image.Width >= Screen.PrimaryScreen.Bounds.Width || img.Image.Height >= Screen.PrimaryScreen.Bounds.Height) {
WindowState = FormWindowState.Maximized;
}
CenterToScreen();
}

Формат зображень
Крім цього з'явилася також необхідність збереження скріншотів в різних форматах (а не тільки PNG як за замовчуванням), благо все це легко вирішується за допомогою все тієї ж функції Bitmap.Save, правда якість jpg зображень мене не влаштувало. Можливість вказати якість у jpg було не так очевидно, швидке гугление і є рішення. Реалізується з допомогою доп параметра EncoderParameter до Bitmap.Save.


// отримуємо енкодер за форматом
private ImageCodecInfo GetEncoder(ImageFormat format)
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs) {
if (codec.FormatID == format.Guid) {
return codec;
}
}
return null;
}

internal void SaveFile(string FilePath, ImageFormat format)
{
var curimg = lastres;
if (format == ImageFormat.Jpeg) {
System.Drawing.Imaging.Encoder myEncoder = System.Drawing.Imaging.Encoder.Quality;
ImageCodecInfo Encoder = GetEncoder(format);
EncoderParameters myEncoderParameters = new EncoderParameters(1);
myEncoderParameters.Param[0] = new EncoderParameter(myEncoder, Properties.Settings.Default.quality);
curimg.Save(stream, Encoder, myEncoderParameters);
} else {
curimg.Save(FilePath, format);
}
}

Також народилася ідея автоматичного відкриття папки після збереження скріншота, а також авто відкриття посилання після завантаження. Швидко це реалізував і додав галочки в налаштування. Ще додав функцію копіювання посилання в буфер обміну.

Після додавання кнопки превью, програма стала виглядати «не так», розташування кнопок було розкидано, подумав трохи, і попереставлял кнопки, так що вийшло наступне:


Дрібні доопрацювання
Трохи передихнувши і подумавши, зрозумів чого ще не вистачає інформації про останню завантаженні скріншота. Зробив відповідне поле, при натисканні на яку можна перейти за посиланням. Крім цього зробив кнопки збереження/редагування недоступними поки не зробиш скріншот. Ну і ще один штрих — додав кнопку «про програму» з коротким описом, версією і датою білду (до речі для отримання дати знову нагуглил рішення, отримуючи дату з заголовка самого додатка).

Загальна після цих дій вийшло наступне:


Трохи пізніше зрозумів що також не вистачає відображення останнього збереженого файлу, що швидко додав, а ще зробив ці поля більш функціональними — прикрутивши контекстне меню при клацанні правою кнопкою миші) де можна було скопіювати посилання/шлях в буфер обміну за допомогою Clipboard.SetText.

Готовність програми, локалізація
Ну і начебто основний функціонал був готовий, все працювало, і подумав я — може поділитися програмою з народом? Якщо робити це, тоді потрібно, як мінімум, зробити можливість локалізації і додати англійську мову. Благо студія легко дозволяє все це реалізувати штатними засобами, почав я все це перекладати. Разом вийшло:


Для перекладу деяких повідомлень потрібно було створити нові файли ресурсів і потім брати з нього рядки наступним чином:


internal ResourceManager LocM = new ResourceManager("ScreenShot_Grab.Resources.WinFormStrings", typeof(MainForm).Assembly);

LocM.GetString("key_name");

Файл з російською мовою у мене WinFormStrings..resx, для англійської WinFormStrings.en..resx, які поклав у папку Resources.

Але щоб змінити мову потрібна перезавантаження програми, звичайно хотілося щоб можна було обійтися без цього, на щастя, є рішення цього питання, яке я швидко застосував. Крім цього також треба було отримати список підтримуваних мов додатком (на майбутнє, якщо раптом будуть ще локалізації), разом нагугли таке рішення, все це об'єднавши вийшла наступна конструкція:

Код зміни мови в реальному часі
private void ChangeLanguage(string lang)
{
foreach (Form frm in Application.OpenForms) {
localizeForm(frm);
}
}

private void localizeForm(Form frm)
{
var manager = new ComponentResourceManager(frm.GetType());
manager.ApplyResources(frm, "$this");
applyResources(manager, frm.Controls);
}

private void applyResources(ComponentResourceManager manager Control.ControlCollection ctls)
{
foreach (Control ctl in ctls) {
manager.ApplyResources(ctl, ctl.Name);
Debug.WriteLine(ctl.Name);
applyResources(manager, ctl.Controls);
}
}

private void language_SelectedIndexChanged(object sender, EventArgs e)
{
var lang = ((ComboboxItem)language.SelectedItem).Value;
if (Properties.Settings.Default.language == lang) return;
UpdateLang(lang);
}

private void UpdateLang(string lang)
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo(lang);
ChangeLanguage(lang);
Properties.Settings.Default.language = lang;
Properties.Settings.Default.Save();
form1.OnLangChange();
}

private void Form2_Load(object sender, EventArgs e)
{
language.Items.Clear();
foreach (CultureInfo item in GetSupportedCulture()) {
var lc = item.TwoLetterISOLanguageName;
var citem = new ComboboxItem(item.NativeName, lc);
//Debug.WriteLine(item.NativeName);
// Задаємо для дефолтного мови свій код і назву в списку
if (item.Name == CultureInfo.InvariantCulture.Name) {
lc = "ru";
citem = new ComboboxItem("Російський", lc);
}
language.Items.Add(citem);
if (Properties.Settings.Default.language == lc) {
language.SelectedItem = citem;
}
}
}

private IList<CultureInfo> GetSupportedCulture()
{
//Get all culture 
CultureInfo[] culture = CultureInfo.GetCultures(CultureTypes.AllCultures);

//Find the location where installed application.
string exeLocation = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path));

//Return all culture for which satellite folder found with culture code.
IList<CultureInfo> cultures = new List<CultureInfo>();
foreach(var cultureInfo in culture) {
if (Directory.Exists(Path.Combine(exeLocation, cultureInfo.Name))) {
cultures.Add(cultureInfo);
}
}
return cultures;
}


Проблема захоплення кордонів у вікна
А тепер я повернуся до проблеми захоплення меж вікна, це питання спочатку було вирішено з допомогою функції автоматичної обрізки вікна (яку я додав в налаштування), вказавши значення для windows 10, але це був радше милицю ніж рішення. Щоб було зрозуміліше про що мова ось скріншот того що я маю на увазі:


(скріншот з більш новою версією)

Як видно на скріншоті — крім вікна захоплювало його межі і те що під ними. Досить довго гугл як вирішити цю проблему, але потім натрапив на цю статтю, де власне описувалося вирішення питання, суть полягає в тому що на windows vista і новіше потрібно використовувати dwmapi для отримання коректних меж вікна з урахуванням aero і тд. З невеликою модифікацією свого коду успішно прив'язав до dwmapi і проблема нарешті була повністю вирішена. Але оскільки функціонал обрізки вікна вже був написаний, вирішив залишити його, можливо комусь буде корисним.

[DllImport(@"dwmapi.dll")]
private static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out Rect pvAttribute, int cbAttribute);

public Bitmap Capture(CaptureMode screenCaptureMode = CaptureMode.Window, bool cutborder = true)
{
...
var handle = GetForegroundWindow();
var rect = new Rect();
// Якщо Win XP і раніше використовуємо старий спосіб
if (Environment.OSVersion.Version.Major < 6) {
GetWindowRect(handle, ref rect);
} else {
var res = -1;
try {
res = DwmGetWindowAttribute(handle, 9, out rect, Marshal.SizeOf(typeof(Rect)));
} catch { }
if (res<0) GetWindowRect(handle, ref rect);
}
...

Підтримка imgur
Потім ще подумавши, раз я збираюся публікувати програму для всіх, то напевно було б непогано крім завантаження на свій сервер зробити завантаження на якийсь сервіс, адже тоді програма буде більш корисною, і не потрібно обов'язково мати свій сервер для її використання, т. к. я давно використовую imgur.com і у нього є простий api, то вирішив зробити прив'язку до нього. Посидівши повивчавши його api спочатку реалізував анонімну завантаження, а трохи пізніше і можливість прив'язки облікового запису. Крім цього реалізував можливість видалення останнього завантаженого зображення в програмі (для їх сервісу тільки).

Повністю описувати код реалізації їх api я не буду, скажу лише що для завантаження зображень на imgur використовував HttpClient і MultipartFormDataContent .NET Framework 4.5 і при цьому я переробив код завантаження зображень на свій сервер, замість бінарної відправки використовував повноцінну завантаження з допомогою форми щоб уніфікувати код. Попутно для свого скрипта як спосіб ідентифікації використовував user-agent і $_GET[key] ключ, що-то не хотілося возитися з повноцінною авторизацією (хоча це по ідеї не складно).


private void uploadfile(bool bitmap = true)
{
byte[] data;
if (bitmap && !imgedit) {
data = BitmapToArray(lastres);
} else {
if (!File.Exists(lastfile)) {
MessageBox.Show(LocM.GetString("file_nf"), LocM.GetString("error"), MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
data = File.ReadAllBytes(lastfile);
}
HttpContent bytesContent = new ByteArrayContent(data);
using (var client = new HttpClient())
using (var formData = new MultipartFormDataContent()) {

...

formData.Add(bytesContent, "image", "image");
try {
var response = client.PostAsync(url, formData).Result;

if (!response.IsSuccessStatusCode) {
MessageBox.Show(response.ReasonPhrase, LocM.GetString("error"), MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
lastlabel.Text = LocM.GetString("error");
lastlabel.Enabled = false;
} else {
...
}

Разом вийшла цілком працездатна і функціональна програма, якої вже можна було робити значно більше речей, ніж я планував робити спочатку.

Список налаштувань на той момент виглядав так:


Сумісність з Win XP
Після я став думати про сумісність з Windows XP, в результаті виявилося що вона підтримує лише .NET Framework 4.0, а MultipartFormDataContent доступний лише v4.5, але її можна як підключити в v4.0 встановивши пакет System.Net.Http. По початку я так і зробив. І начебто все добре, крім того що на Windows Vista/7 потрібно встановлювати .NET Framework 4.0 для того, щоб програма запрацювала. Перемкнув проект на 3.5, переписав завантаження зображень на WebClient, і замість завантаження файлу використовував звичайне поле з закодованим зображенням у форматі base64, благо api у imgur дозволяє також завантажувати зображення, так і переписати свій php скрипт не склало праці під цей варіант. А потім вирішив також переключити проект на версію 2.0, і в підсумку банальної правкою пари рядків отримав повністю робочий .NET Framework 2.0 проект.


using (var client = new WebClient()) {
var pdata = new NameValueCollection();

...

pdata.Add("image", Convert.ToBase64String(data));

try {
var response = client.UploadValues(url, "POST", pdata);
var result = Encoding.UTF8.GetString(response);

...


$file = base64_decode($_POST["image"]);

Це все дозволило запускати програму на старих фреймворках, а на Windows Vista/7 запускати без установки чого-небудь, оскільки згідно цієї статті Windows Vista містить v2.0, а Windows 7 містить v3.5 за замовчуванням. Але на цьому проблеми не закінчилися. На Windows 8 і новіше початок просити установку .NET Framework v3.5, що звичайно погано, але питання було швидко вирішив завдяки цієї інформації, підправивши опції supportedRuntime в конфіги, дозволяючи запускати програму на нової чи старої версії без будь-яких проблем. Крім цього зробив можливість використання протоколу TLS 1.2 якщо він доступний (тобто на системах .NET Framework 4.5).

app.config

<startup>
<supportedRuntime version="v4.0"/>
<supportedRuntime version="v2.0.50727"/>
</startup>

Підтримка TLS 1.2


System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls;
try {
System.Net.ServicePointManager.SecurityProtocol |= (SecurityProtocolType)3072; //SecurityProtocolType.Tls12;
} catch { }

Історія подій
За великим рахунком думав що все, на це вистачить, можна релізувати, але всеж раніше чогось не вистачало — історії дій з логом. Почав розробку відповідного вікна з деякими функціями, як видалення файлу з пк і imgur, відкриття файла/посилання, копіювання шляху/посилання з допомогою контекстного меню. Також зробив можливість зберегти події в лог файл зі списку, так і автоматично задавши в налаштуваннях.

Вийшло цілком інформативне вікно:


Проблема в роботі HookCallback на Win XP
Але вилізла одна проблема — на Windows XP при захопленні сркиншотов запис додавалася двічі. У ході тестів з'ясував, що HookCallback викликається двічі при відпусканні клавіші, причина такої поведінки мені була не зрозуміла, але вирішив питання досить легко — зробив додаткову перевірку натискання клавіш, зберігаючи це в змінну, а при відпусканні клавіші зміна змінної false, у результаті потрібний мені код став оброблятися лише 1 раз при відпусканні клавіші.


private static bool pressed = false;

private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
if (nCode >= 0) {
Keys number = (Keys)Marshal.ReadInt32(lParam);
//MessageBox.Show(number.ToString());
if (number == Keys.PrintScreen) {
if (pressed && wParam == (IntPtr)261 && Keys.Alt == Control.ModifierKeys && number == Keys.PrintScreen) {
var res = Scr.Capture(ScreenCapturer.CaptureMode.Window, Properties.Settings.Default.cutborder);
WinForm.OnGrabScreen(res, false, true);
pressed = false;
} else if (pressed && wParam == (IntPtr)257 && number == Keys.PrintScreen) {
var res = Scr.Capture(ScreenCapturer.CaptureMode.Screen);
WinForm.OnGrabScreen(res);
pressed = false;
} else if (wParam == (IntPtr)256 || wParam == (IntPtr)260) {
pressed = true; // fix for win xp double press
}
}
}

return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
}

Проблема захоплення скріншотів з ігор
Трохи пізніше в ході тестування зіткнувся з проблемою захоплення скріншотів з повноекранних додатків (наприклад, ігри), помітив що в windows 10 штатний printscreen захоплює це справа без проблем, в результаті додав функцію вставки зображення з буфера обміну, а також додав галочку «використовувати буфер обміну замість захоплення» у налаштування, тим самим «вирішив питання» для себе, але як виявилося в win 7 і нижче не працює, почав вивчати питання, і зрозумів що це досить складне завдання, з необхідністю використання directx ін'єкцій, у підсумку просто забив на цю проблему, все-таки основна мета не захоплення скріншотів з ігор, для цього існує безліч інших програм та інструментів.

Попутно додавши налаштування переробив меню налаштувань, зробив його більш компактним щоб вміщалося на екран з роздільною здатністю 640*480 пікселів, і воно стало виглядати так:


Також зробив більш функціональним іконку в треї, додавши туди всі важливі функції при клацанні правою кнопкою:


Перевірка на Win98 і Win2000
Ну і вже чисто заради експерименту розгорнув на виртуалке windows 2000 SP4 і 98 SE, поставив там .NET Framework 2.0. Це було зробити не так просто, оскільки потрібна установка деяких патчів і оновити Windows Installer. Але все ж таки все вийшло і я спробував запустити додаток.

Як виявилося на Windows 2000 SP4 додаток виявилося повністю робочим, а ось на Windows 98 SE захоплення клавіш не працював, вставка з буфера теж не працює, однак завантаження скріншота з файлу працює без проблем. Власне ці проблеми вирішити не вийшло, інформації вкрай мало, все що зміг з'ясувати — параметр «WH_KEYBOARD_LL» додали лише в Windows 2000. А про причини не працює вставки зображення з буфера взагалі не знайшов ніякої інформації. Разом хв вимоги — Windows 2000.

Отже після деяких перевірок, дебага і дрібних фіксів програма була готова нарешті, і фінальний варіант виглядає так:


Все що залишилося — створити github репозиторій, завантажити вихідні коди, скомпилить додаток, написати рідмі і зробити реліз. На цьому історія розробки закінчується. Готову програму можна завантажити та переглянути вихідний код на GitHub. Сподіваюся стаття була корисною.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.