Если бы microsoft не была такой ленивой...

Как-то в предыдущих статьях я уже упоминал о прекрасной (и полезной!) возможности операционной системе линукс, суть которой заключается в том, что можно окну любого приложения придать статус "Поверх всех". Казалось бы мелочь, но в реальной жизни очень удобно. И очень жаль, что в Windows такой возможности нет. Даже подумываю утилитку написать, которая будет это реализовывать.

Сегодня возникла еще одна идея, реализовав которую, microsoft смогла бы заработать нехилую благодарность, как от пользователей, так и от программистов. Особенно от программистов.

Дело в том, что рано или поздно перед разработчиками прикладных программ встает проблема проверки этого самого правописания в вводимом пользователями тексте. Вот и я тоже наступил на эти грабли.

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

Отечественные наработки не сильно лучше. Причина, видимо, в том, что они в основе своей содержат все те же западные поделки, которые наши умельцы слегка "адаптировали" под русский. В результате получаем тот же косяк, только вид "с боку".

Наилучшим решением в данной ситуации является использование механизма из уже упомянутого microsoft Office. Как ни странно, но офисный алгоритм от майкрософта справляется с русским гораздо лучше всяких сторонних изобретений. Даже странно как-то. Но факт.

Ну что ж, офис, так офис! Тем более, что большинство разработчиков именно его модули и использует.

Начинаю копать. Первым делом естественно MSDN. Попытка найти нужное решение "в лоб" терпит крах (как обычно), потому начинаю думать головой.

Теоретически к офису обратиться можно через технологию COM (Component Object Model), значит роем MSDN на тему привязки COM-объектов к C#-коду. И тут же получаем готовый пример именно проверки правописания в свобственной программе, используя механизм из microsoft Word. Вот это удача!

Правда без косяков не обошлось. Пример, описанный во всех деталях в MSDN, у меня просто не откомпилировался. :)

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

Замеченные в первые минуты неудобства:

  • Проверка осуществляется только над всем текстом целиком и состоит в вызове стандартного окна замены неверно написанных слов. Следствие – очень ограниченная функциональность.
  • При каждой проверке запускается сам Word (хоть и в фоне в невидимом окне) и по окончании проверки убивается. Следствие – тормоза.
  • После окончания проверки и перед тем как окно Ворда будет убито, оно странным образом появляется на экране, хотя при создании ему ясно указали быть невидимым. Следствие – моргание полноэкранного окна ворда поверх вашей программы при каждой проверке орфографии.
  • Для связи с вордом используется промежуточная сборка (в умных книжках ее зовут RCWRuntime COM Wrapper или "COM-оболочка времени выполнения" - на мой взгляд весьма жудкое название). Т.е. связь с вордом устанавливается еще в момент запуска программы (ранне связывание) и, если на машине клиента вдруг ворда не кажется или окажется, но другой версии, то ваша программа покажет пользователю большую фигу в виде фолта. А пользователи фиг очень не любят. Следствие – катастрофическая зависимость от наличия определенной версии ворда в системе.
  • Сама RCW представляет собой не что иное как DLL-файл, который придется таскать вместе с программой (без него тоже получите фолт, причем уже вне зависимости от наличия ворда на машине клиента). Размер у этой библиотеки ни много, ни мало, а целых пол-мегабайта. И это лишь для передачи вызовов из NET в COM и обратно. Стоит задуматься.

Короче, предлагаемый майкрософтом вариант меня полностью не устроил. Мне бы хотелось, чтоб выполнялись следующие условия:

  • Была возможность определять правильность отдельных слов, без окон замены и т.д. Программе нужно лишь определить, правильно написано это слово или неправильно. Весь остальной механизм с заменами и словарями – дело техники.
  • Для работы программа использовала бы только одну копию ворда, т.е. при старте программы он загружается, при завершении программы – выгружается
  • Никаких внешних признаков присутствия ворда – окошек, мограний и т.д.
  • Нормальная обработка отсутствия ворда в системе. Т.е. даже если офис не установлен, программа должна корректно работать (естественно уже без проверки орфографии).
  • Отсутствие потребности в каких бы то ни было дополнительных сборках-библиотеках. Тем более таких диких размеров.

Согласитесь, требования весьма скромные. Значит и реализация должна быть достаточно простой. А вот собственно и она:

Пример класса:
/*********************************/
/* Simple Spell Checker          */
/* Copyright (C) FIT-Media, 2008 */
/* http://fit-media.com          */
/*********************************/

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;

public class SpellChecker : IDisposable
{
	System.Type TWord = null; object com_app = null;

	private static SpellChecker Checker = new SpellChecker();
	private SpellChecker()
	{
		try {
			TWord = Type.GetTypeFromProgID("Word.Application");
			com_app = Activator.CreateInstance(TWord); 
		}
		catch { com_app = null;}
	}

	public static SpellChecker GetChecker() { return Checker; }
	public void Dispose()
	{
		if (com_app != null) {
			object[] arg = { null, null, null };
			TWord.InvokeMember("Quit", BindingFlags.InvokeMethod, 
				null, com_app, arg);
			com_app = null;
		}
	}
	public bool CheckWord(string word)
	{
		object[] arg = { word };
		return (bool)TWord.InvokeMember("CheckSpelling",
			BindingFlags.InvokeMethod, null, com_app, arg);
	}
}

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

Единственное, на что стоит обратить внимание, это необходимость вызова метода SpellChecker.Dispose() при завершении работы вашей программы. Иначе запущенная копия ворда останется в памяти (привет нормальным пацанам из microsoft и их гениальному сборщику мусора) и будет болтаться там до перезапуска системы.

А-а-а, сейчас глянул на начало статьи и понял, что начал об одном, а потом как-то незаметно пришел совсем к другому. :)

Если вы не против, то я все же вернусь к рассказу о моей идее и лени мелко-мягких.

Суть идеи сводится к "глобализации" системы проверки правописания, которая уже встроена в microsoft Office. Прикол в том, что практически 100% программ используют для ввода текста элементы управления, встроенные в операционную систему.

Так вот, достаточно встроить механизм проверки орфографии в саму ОС и подключить его к двум основным средствам ввода текста (в терминах NET Framework это TextBox и RichTextBox), как абсолютно все программы, без какого бы то ни было вмешательства разработчиков, приобретут возможность отображения неверно написанных слов. А перед счастливыми программистами больше никогда не встанет задача реализации в своих программах этой самой "проверки грамотности ввода".

Кстати, при таком подходе словари тоже окажутся глобальными, т.е. добавили вы в словарь слово в ворде и его уже опознают все остальные программы. Здорово? Конечно!

Вот только дети дяди Билла толи сильно ленивые, толи очень жадные (что скорее всего) и ни за что не хотят дать нам всем такую замечательную возможность. :(


Залепа №13. О грамотном проектировании и неграмотном IDE.

Эту статью без мата в адрес мелкомягких писать сложно :( Но я все же постараюсь.

Задача:

Есть некая программа, работающая с базами данных. Естественно в ней куча таблиц, большинство из которых представляют собой достаточно простые справочники. Например, это могут быть справочники телефонных кодов, типов продукции, видов доставки, да чего угодно. Для простоты будем рассматривать только один из таких справочников - справочник пользователей.

Каждая запись справочника содержит данные об одном пользователе: его логин, пароль для входа в систему и какие-то дополнительные атрибуты. Например его права доступа. В программе, кроме основных функций, должна быть возможность как редактирования списка пользователей так и изменения параметров отдельных пользователей.

Задача, согласитесь, тривиальна и встречается на каждом шагу. Попробуем ее решить средствами C#. Вариантов решений может быть масса, но нас интересует грамотный подход, т.е. такое решение, при котором наша программа не превратится в кашу из кучи классов, лишних открытых методов, свойств и форм.

Здравый смысл подсказывает, что справочник должен быть представлен неким классом (назовем его CUserMgr), который позволяет основной программе получить все необходимые данные о пользователе и скрывает всю внутреннюю структуру внутри себя.

Опять же для простоты решим, что программе нужно получать только лишь логин и пароль пользователя по его идентификатору. Ну и конечно вызывать редактор пользователей. Таким образом у нас получается примерно такой открытый интерфейс нашего класса:

Набросок класса
public class CUserMgr
{
	// получить идентификатор по логину
	public int GetID(string login) {...}
	
	// получить логин по идентификатору
	public string GetLogin(int id) {...}
	
	// получить пароль по идентификатору
	public string GetPassword(int id) {...}
	
	// вызвать редактор пользователей
	public void ShowEditor() {...}
}

Так же класс должен содержать еще набор закрытых методов для редактирования пользователей: добавление нового пользователя, удаление существующего, изменение параметров пользователя, загрузку и сохранение списка пользователей и т.д.

Редактор пользователей должен отобразить на экране окно списка пользователей, в котором можно добавить нового пользователя и удалить или отредактировать существующего. Для целей добавления/редактирования будет использоваться еще одно окошко - форма редактирования пользователя. Итак, получаем как минимум две формы, которые должны напрямую обращаться к внутренней структуре класса CUserMgr, т.е. к его закрытым методам.

Решение "в лоб", т.е. создание глобальных классов нужных нам форм ни к чему хорошему не приведет:

  1. о существовании этих форм должен знать только CUserMgr, остальные о них не должны даже подозревать, т.к. никому кроме CUserMgr они не нужны. У нас же они получаются глобальными, т.е. видимыми всеми классами. Это плохо.
  2. Чтобы формы могли обратиться к внутренней структуре класса CUserMgr придется внутреннюю структуру сделать открытой, т.к. друзья (friend) в C# не предусмотрены (об этом уже писалось здесь). Открытая структура класса менеджера приводит к путанице и соблазну использовать эти методы (которые по логике должны быть закрытыми) не по назначению. А как же инкапсуляция??? В этом случае ее просто невозможно реализовать! Ужас!

Все это можно перетерпеть если вы создаете класс, который будет использоваться только в вашей программе и код которого никогда не будет использован где-либо еще. А как же быть, если вы создаете коммерческую библиотеку? Неужели придется вот так вывалить на всеобщее обозрение потроха ваших классов? Это уже очень плохо. Получается, что в "самом объектно-ориентированном языке современности" (а именно так microsoft позиционирует его на рынке) использовать всю мощь этой самой объектно-ориентированности и не получится!

Изрядно поломав голову, я смог найти еще два решения: первое в стиле майкрософта - корявое и громоздкое, второе - единственно верное для C# и правильное с точки зрения ООП, но имеющее изрядный косяк при использовании IDE Microsoft Visual Studio. Разберем оба варианта.

Вариант номер раз (стиль программирования a'la Microsoft)

Суть заключается в избавлении от необходимости использовать одним классом внутренней структуры другого класса. Решение сводится к созданию классов, представляющих сущности используя в качестве базы класс формы. Мудрено? Да!

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

Набросок класса записи
public class CUser : Form
{
	private string Login;
	private string Password;
	private int ID;
	...

	// конструктор
	public CUser(int id, string login, string passw) {...} 

	public string GetLogin() { return Login; }
	public string GetPassword() { return Password; }
	public int GetID() { return ID; }

	// вызов редактора пользователя
	public void Edit() {...}
	// загрузка и сохранение данных о пользователе
	public bool Load() {...}
	public bool Save() {...}
}

В общем нечто такое. Теперь нашему окну редактирования пользователя (классу CUser) уже не нужно обращаться к классу CUserMgr за информацией - все необходимое он содержит в себе.

Идем далее. Наш класс менеджера также должен быть наследником класса формы (Form). В этом случае он сможет так же отобразить себя для редактирования общего списка пользователей. И ему также не нужно будет обращаться к внутренней структуре других классов, т.к. список пользователей (объектов класса CUser) он содержит внутри себя. Он их будет создавать и удалять (редактирование списка пользователей), получать из них информацию и передавать ее в основную программу.

Вот примерно такой подход.

Согласитесь, что такая убогая симуляция дружественности классов имеет массу недостатков:

  1. Нормальной инкапсуляции все равно не получится, т.к. в реальной программе некоторые закрытые методы как CUser так и CUserMgr все же придется открыть.
  2. Класс CUser все еще глобален и видим любым и каждым со всеми вытекающими отсюда отрицательными последствиями.
  3. Для справочника пользователей, которых не больше сотни этот подход будет работать более-менее нормально. Но если это будет справочник артикулов продукции, которых может быть несколько десятков тысяч, то мы получи дикий перерасход памяти, ведь для каждого объекта будет создано отдельное (пусть и скрытое) окно. Это вызовет не только расход памяти, но и будет существенно тормозить программу.

Короче, это не выход, а скорее его имитация. Выкрутас, временная отмазка. В реальных крупных программах не советую его использовать.

Вариант номер два (реально объектно-ориентированный)

Второй вариант использует возможность C# создавать вложенные классы. Видимо их ввели в язык только с одной целью - как решение при отказе от дружественности классов. Тут надо сделать некоторые пояснения.

В C# введена возможность определить класс внутри другого класса. При этом встроенный класс получает статус "члена класса-оболочки", т.е. все объекты вложенного класса (кем бы и где бы они не были созданы) имеют ПОЛНЫЙ доступ к членам своего класса-оболочки: они могут использовать и защищенные и даже закрытые методы класса-оболочки. Поразмыслите над эти на досуге и найдете эту возможность достаточно удобной, хотя и несколько путанной в применении.

Как же будет выглядеть решение нашей задачи при использовании вложенных классов? А примерно вот так:

Объектно-ориентированное (правильное) решение
public class CUserMgr
{
	class CUser
	{
		private string Login;
		private string Password;
		private int ID;
		...
	}

	class CUserEdit : Form
	{
		public void Edit();
		...
	}

	class CUserList : Form
	{
		public void ShowList() {...}
		...
	}

	// закрытые методы и свойства
	...

	// "внешний интерфейс"
	public bool Load() {...}
	public bool Save() {...}
	public void ShowEditor() {...}
}

Итак, вот что мы получаем при таком подходе:

  1. программа "видит" только класс CUserMgr, о существовании внутренних классов она даже не подозревает и естественно не может получить к ним доступ. Она может использовать только открытый "внешний интерфейс" класса CUserMgr. Внутренняя структура нашего менеджера пользователей полностью скрыта (инкапсуляция торжествует!).
  2. Можно как угодно изменять внутреннюю структуру CUserMgr и вложенных классов. Если внешний интерфейс не изменится, то эти модификации никак не скажутся на работе основной программы, использующей наш класс.
  3. Встроенные классы могут без проблем обращаться к внутренним (закрытым) методам класса CUserMgr, что позволяет в ряде случаев сильно упростить программирование этих классов.

В общем мы добились нормального решения поставленной задачи. Поздравляю.

"А где же косяк?" - спросите вы - "неужели microsoft наконец-то сделала все правильно?" :)

А косяк все же есть. Но на этот раз он не в языке, а в IDE Visual Studio.

Классы, созданные по описанной выше "технологии" будут нормально компилироваться и работать (а как же иначе - все ведь сделано верно, в соответствии с требованиями и возможностями языка C#). Но в IDE вы не сможете использовать визуальный редактор для форм, которые являются вложенными в другие классы (в нашем случае это CUserEdit и CUserList) - IDE вместо отображения формы просто будет материться по-английски. Поэтому проектирование пользовательского интерфейса превратится при описанном подходе в настоящую муку. :(

Почему так происходит, ведь с точки зрения языка все верно и никакого "криминала" в таком вложении нет? Этот вопрос нужно адресовать разработчикам Visual Studio, которые (как обычно) лучше нас с вами знают что и как должно работать.


Залепа №12. Microsoft не знает собственного кода.

Безусловно, чем больше различных свойств, методов и событий предусмотрели для класса его создатели, тем более гибок он будет в использовании. По крайней мере так должно быть. По крайней мере в теории. Но практика вещь суровая.

Обратим свое внимание на набор обрабатываемых любым контролом событий, связанных с клавиатурой. Типичный TextBox содержит 4 штуки: KeyDown, KeyPress, KeyUp и PreviewKeyDown. Последнее оставим для другого раза, а вот первые три изучим поподробнее.

KeyDown и KeyUp срабатывают соответственно при нажатии и отпускании клавиш. При этом в обработчик передается вся необходимая для точного определения клавиши и состояния клавиатуры информация. Даже с избытком, некоторые свойства просто дублируют другие. Ну да ладно.

Недостатком здесь служит тот факт, что обработчик получает коды клавишей, а не коды символов. Соответственно они никак не зависят от состояния "включенности" CapsLock и NumLock, равно как и от текущей раскладки (языка ввода). Это приводит к серьезным проблемам при написании программ работы с текстом, которым нужны не коды клавиш, а именно коды набираемых пользователем символов.

Для решения проблемы разработчики и ввели событие OnKeyPress. Фактически оно наступает, когда в буфер клавиатуры попадает очередной символ и обработчик этого события получает как раз код символа, а не клавиши. Соответственно это событие не наступает для нажатий/отпусканий таких клавиш как Shift, Alt, Ctrl, F1-F12, Tab и т.д.

Стоит сказать еще об одной вещи. И в обработчике OnKeyDown и в OnKeyPress существует (слава Богу!) возможность указать системе, что принятый символ не должен проходить дальнейшую обработку и должен покинуть очередь вода. Таким образом можно "фильтровать базар", т.е. отсеивать из вводимого пользователем бреда то, что заведомо ложно. Например, если требуется ввести сумму, то нужно из потока ввода удалить все символы кроме цифр, точки, минуса и возможно кода клавиши "Забой" (BackSpace).

Так вот, суть сегодняшнего прикола в примере из MSDN, в котором бравые парни из мега-крутой конторы в очередной раз показывают как нужно правильно пользоваться их библиотекой. Вот этот пример со всеми комментариями:

Пример кода:
// Boolean flag used to determine when a 
// character other than a number is entered.
private bool nonNumberEntered = false;

// Handle the KeyDown event to determine the 
// type of character entered into the control.
private void textBox1_KeyDown(object sender, KeyEventArgs e)
{
    // Initialize the flag to false.
    nonNumberEntered = false;

    // Determine whether the keystroke is a number from 
    // the top of the keyboard.
    if (e.KeyCode < Keys.D0 || e.KeyCode > Keys.D9)
    {
        // Determine whether the keystroke is a number 
        // from the keypad.
        if (e.KeyCode < Keys.NumPad0 || e.KeyCode > Keys.NumPad9)
        {
            // Determine whether the keystroke is a backspace.
            if(e.KeyCode != Keys.Back)
            {
                // A non-numerical keystroke was pressed.
                // Set the flag to true and evaluate 
                // in KeyPress event.
                nonNumberEntered = true;
            }
        }
    }
}

// This event occurs after the KeyDown event and 
// can be used to prevent characters from entering 
// the control.
private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
    // Check for the flag being set in the KeyDown event.
    if (nonNumberEntered == true)
    {
        // Stop the character from being entered into 
        // the control since it is non-numerical.
        e.Handled = true;
    }
}

Как видно из примера, для такой простой вещи, как удаление из потока заведомо лишних символов, требуется писать обработчики для сразу ДВУХ событий, да еще и переменную использовать. Иначе нельзя - работать наверное не будет. По крайней мере они другого варианта не узрели. Кстати вложенные сравнения им тоже видно не по зубам. :)

Ладно, вот еще вариант, на этот раз мой:

Пример кода:
private void textBox1_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)
{
  if (!((e.KeyCode >= Keys.D0 && e.KeyCode <= Keys.D9) ||
        (e.KeyCode >= Keys.NumPad0 && e.KeyCode <= Keys.NumPad9) ||
         e.KeyCode == Keys.Separator || e.KeyCode == Keys.Decimal || 
         e.KeyCode == Keys.Back || e.KeyCode == Keys.Subtract))
     e.SuppressKeyPress = true;
}

Результат работы обоих примеров одинаковый, только у меня переопределен только один обработчик, не нужна дополнительная переменная, да еще и ввод символа "минус" разрешен, равно как и десятичной точки и десятичной запятой. :)

Вывод этого краткого поста таков:

  1. разработчики библиотеки до такой степени напутали с лишними свойствами и методами, что сами потерялись в этой головоломке;
  2. из-за разработчиков библиотеки, которые "забыли" дополнить TextBox возможностью фильтровать (хотя бы на уровне "цифра/буква") пользовательский ввод, нам с вами приходится делать финты ушами, чтобы реализовать такие полезные в каждодневной работе вещи.

    О MaskedTextBox прошу мне не напоминать - с глюками этого контрола разберемся отдельно как-нибудь позже.

Page: [1] [2] [3] [4] [5] [6]

Этот сайт полностью окупает себя, хотя его ТИЦ=10, а PR=2. Хотите знать, как он это делает? Хотите чтобы Ваш сайт чарез пол-часа тоже начал на полном автопилоте приносить деньги?
Регистрируйся здесь и здесь и начинай получать деньги со своего сайта!