Синопсис
Не так давно начал изучать C# и очень скоро эксперименты переросли в желание написать какое-нибудь легкое, простое, но вместе с тем полезное и удобное приложение. Постепенно родилась идея программы, предназначенной для быстрого снятия скриншотов и автоматической загрузки их на хостинг. Удовлетворяющих моим требованиям аналогов я не нашел, поэтому я решил все же сделать её самостоятельно, а уже после этого один хороший человек подал идею написать статью об этом.
Суть
Итак, что моя программа умеет?
- Снимает скриншоты в один клик: по нажатию «горячих клавиш» (Ctrl+Print Screen) либо по клику на иконке в трее.
- Сохраняет изображения (очевидно) с именами файлов, содержащими дату и время.
- Тут же автоматически загружает их на Imgur.com и выдает ссылку во всплывающем окне.
- При нажатии на всплывающее окно загруженная картинка открывается в браузере.
- Есть возможность скопировать ссылку в буфер обмена (через контекстное меню программы).
- Можно, тоже из контекстного меню, открыть папку с сохраненными скриншотами в Проводнике.
- И наконец, легко и быстро включаемый автозапуск приложения при загрузке системы.
Трудности
А теперь часть, которая, возможно, кому-то покажется очевидной и банальной, а кому-то может помочь. Даже при разработке такого небольшого приложения, как оказалось, невозможно избежать сложных моментов.
Загрузка изображения на хостинг
Первым, с чем я столкнулся, была ошибка с использованием API хостинга: изображение загружалось, но куда-то девались последние несколько килобайт файла. Вначале использовал следующий код, найденный среди примеров использования API на самом хостинге:
using System;
using System.IO;
using System.Net;
using System.Text;
namespace ImgurExample
{
class Program
{
static void Main(string[] args)
{
PostToImgur(@"C:\Users\ashwin\Desktop\image.jpg", IMGUR_ANONYMOUS_API_KEY);
}
public static void PostToImgur(string imagFilePath, string apiKey)
{
byte[] imageData;
FileStream fileStream = File.OpenRead(imagFilePath);
imageData = new byte[fileStream.Length];
fileStream.Read(imageData, 0, imageData.Length);
fileStream.Close();
string uploadRequestString = "image=" + Uri.EscapeDataString(System.Convert.ToBase64String(imageData)) + "&key=" + apiKey;
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create("http://api.imgur.com/2/upload");
webRequest.Method = "POST";
webRequest.ContentType = "application/x-www-form-urlencoded";
webRequest.ServicePoint.Expect100Continue = false;
StreamWriter streamWriter = new StreamWriter(webRequest.GetRequestStream());
streamWriter.Write(uploadRequestString);
streamWriter.Close();
WebResponse response = webRequest.GetResponse();
Stream responseStream = response.GetResponseStream();
StreamReader responseReader = new StreamReader(responseStream);
string responseString = responseReader.ReadToEnd();
}
}
В коде были ошибки, но даже после их исправления проблема не исчезла. После долгих и мучительных поисков ошибки и множества перепробованных вариантов я пришел к гипотезе, что файл может обрезаться после кодирования из-за слешей, используемых в base64. Возможно, они интерпретировались как escape-символы, но точно выяснять это было уже лень: пришлось бы сниффером ловить, что именно уходит в сеть (в отладчике все было отлично вплоть до вызова streamWriter.Write(uploadRequestString)), сравнивать base64-последовательности и т.п. В интернете эта проблема конкретно не освещалась, но попалась пара постов, в которых тоже были подозрения на слеши.
В итоге я использовал второй вариант кода, найденный на StackOverflow:
using (var w = new WebClient())
{
var values = new NameValueCollection
{
{ "key", "433a1bf4743dd8d7845629b95b5ca1b4" },
{ "image", Convert.ToBase64String(File.ReadAllBytes(@"hello.png")) }
};
byte[] response = w.UploadValues("http://imgur.com/api/upload.xml", values);
Console.WriteLine(XDocument.Load(new MemoryStream(response)));
}">
Он уже работал как часы, потребовалось только адаптировать его под свои нужды — функция должна была возвращать только ссылку, а не весь ответ.
Горячие клавиши
Вторая сложность возникла с горячими клавишами. Везде предлагается для перехвата глобальных горячих клавиш вешать хук функцией RegisterHotKey, а затем переопределять WndProc и в ней уже воспринимать сообщение. Казалось бы, в чем может быть сложность здесь? Сложность возникла в том, что мое приложение безоконное (хоть и WinForms), стало быть, нет ни дескриптора окна для передачи в RegisterHotKey, ни самого окна, в котором можно было бы переопределить WndProc. После напряженного курения мануалов выяснилось, что дескриптор передавать необязательно — в этом случае сообщения будут передавать не окну, а вызвавшему RegisterHotKey потоку. Первая часть проблемы была решена, но ловить само сообщение все еще было нечем. После еще пары стаканов кофе нашел решение и для этой проблемы: Application.AddMessageFilter(). Для того, чтобы ею воспользоваться, необходимо реализовать «фильтр» сообщений, в моем случае, он ловил WM_HOTKEY и вызывал соответствующую процедуру для обратки нажатия в контексте приложения. Больше препятствий для разработки не было.
Fin
Приложение, разумеется, freeware и исходники я тоже решил сделать открытыми. Комментарии оставлял в тех местах кода, которые, по моему мнению, неочевидны сами по себе. Поддержки локализаций нет, язык интерфейса английский. Приложение написано в VS2010 под .NET 4.0, никаких сторонних библиотек использовано не было. Иконка была найдена на iconfinder.com. Я не претендую на уникальность идеи, программа была написана для набивания руки, в процессе которого я столкнулся с определенными задачами и описал их решение в статье.
UPD. Залил бинарник отдельно, чтобы можно было пользоваться, не качая весь репозиторий: ScreenPaste.exe