Category: it

forest, summer

Преобразование HTML в XHTML на C и C++

Задача преобразования HTML-форматированного текста в XHTML появилась в связи с тем, что нужно было вставлять в XML блоки форматированного текста. (Про чтение таких блоков при SAX-разборе я написал статью: http://lj.rossia.org/users/shestero/141287.html ). Эти вставки писались вручную и часто содержали ошибки форматирования, невидные при просмотре в браузерах, но неприемлимые для строгих парсеров XML. Кроме того в исходных текстах часто были непарные теги, вроде <BR>, которые каждый раз приходилось отыскивать и исправлять вручную.

Для выполнения этой задачи на C++ я успешно приминил бесплатную библиотку Tidy. К сожалению разработка её остановилась в начале 2009, но для моей задачи она сгодилась.

Я использовал MinGW, компилировал из коммандной строки-консоли Qt 4.8.3 под Windows 7.
Так как исходники уже довольно древние, при компиляции возникают небольшие загвоздки. Вот пошаговая инструкция, как я делал:
1. Использовал последний tarbar-архив из CSV (март 2009).
2. После распаковки надо создать вручную директории obj (где Makefile) и bin c lib (в директории tidy) (сами они не создаются)
3. Я использовал Makefile для gmake, но его пришлось изрядно поправить. Также для компиляции в MinGW пришлось поправить директиву выбора блока в файле src/mappedio.c. Исправленные файлы я выложил в архив tidy-changes.7z [здесь].
4. Для сборки библиотеки запустите make из директории build\gmake.

Вот код на C++, обеспечивающий конвертацию:
// Convert HTML to XHTML and clean up using libTidy
#include <tidy.h>
#include <buffio.h>
string CleanHTML(const char *html)
{
    // Initialize a Tidy document
    TidyDoc tidyDoc = tidyCreate();
    TidyBuffer tidyOutputBuffer = {0};

    // Configure Tidy
    // The flags tell Tidy to output XML and disable showing warnings
    bool configSuccess = tidyOptSetBool(tidyDoc, TidyXmlOut, yes)
        && tidyOptSetBool(tidyDoc, TidyQuiet, yes)
        && tidyOptSetBool(tidyDoc, TidyNumEntities, yes)
        && tidyOptSetBool(tidyDoc, TidyShowWarnings, yes) // no
        ;//&& tidyOptSetValue(tidyDoc,TidyCharEncoding, "utf8");

    int tidyResponseCode = -1;

    // Parse input
    if (configSuccess)
        tidyResponseCode = tidyParseString(tidyDoc, html);

    // Process HTML
    if (tidyResponseCode >= 0)
        tidyResponseCode = tidyCleanAndRepair(tidyDoc);

    // Output the HTML to our buffer
    if (tidyResponseCode >= 0)
        tidyResponseCode = tidySaveBuffer(tidyDoc, &tidyOutputBuffer);

    // Any errors from Tidy?
    if (tidyResponseCode < 0) // Tidy encountered an error while parsing an HTML
        throw tidyResponseCode;

    // Grab the result from the buffer and then free Tidy's memory
    std::string tidyResult = (char*)tidyOutputBuffer.bp;
    tidyBufFree(&tidyOutputBuffer);
    tidyRelease(tidyDoc);

    return tidyResult;
}
// ................

        // Convert HTML to XHTML and clean it
        // see also: https://bugs.webkit.org/show_bug.cgi?id=44876
        string xhtml;
        try
        {
            xhtml = CleanHTML( html );
        }
        catch (int e)
        {
            cerr << "Clean HTML error (from libTidy): " << e << endl;
        }
Добавлю, что в моём случае обработка UTF-8 символов (кирилицы) в приложении, запущенном под Windows 7 x64 происходила не правильно (они конвертировались в кодовые &-представления побайтно). Так как мне это не было нужно в тот момент, я не стал разбираться в чём там дело, возможно просто в системных настройках локалей или в каких-то параметрах запуска библиотечных функций или компиляции. Вроде бы Tidy поддерживает UTF-8. Для обработки кириличных HTML-текстов также можно перевести их в однобайтовую кодировку, например в CP-1251.

Для C++ также существует специальная обёртка Tidy-библиотеки TidyPP: http://code.google.com/p/tidypp
forest, summer

Qt: сокрытие вкладок в QTabWidget

В стандартном виджете для организации вкадок в Qt нет возможности сокрытия отдельных вкладок (show/hide). Интернете не нашёл решения – предлагают сложные пути: временно удалять вкладки или переделывать виджет.

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

Идея в том, что бы ставить режим disabled вкладкам, которые надо скрыть, а для маскировки вкладок использовать приём задание стиля с нулевым размером вкладки. Причём при установке режима disabled виджет автоматически переключит активную вкладку.

Нужный стиль устанавливается так:
#include "qtesttabwidget.h"

#include <QTabBar>

QTestTabWidget::QTestTabWidget() // наследник QTabWidget
{
    setStyleSheet();
}

void QTestTabWidget::setStyleSheet()
{
    tabBar()->setStyleSheet(
        "QTabBar::tab:disabled { width: 0; height: 0; right: 1px; }" //  ??? border-style: none; margin-left: 1px;
    );
}
У меня чуть-чуть кривовато выглядит последняя граница последней видимой вкладки, если она не активная и если скрыть самую последнюю. Вероятно тут можно как-то добиться идеального отображения и в этом случае поколдовав с полями, смещениями итд в стиле. Если кто разберётся с этим — пожалуйста, отправьте мне результат!
Состояние «:disabled» вроде не документировано для QTabBar::tab, однако у меня в Qt 4.5.0 это работает, думаю, и в новых версиях тоже должно.

Вот тестовая программа (код класса главного окна QMainWindow), которая динамически скрывает/показывает вкладки:
#include "mainwindow.h"

#include <QCheckBox>
#include <QVBoxLayout>
#include <QHBoxLayout>

#include <QLabel>
#include <QTabBar>


MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QWidget* w = new QWidget(this);
    QVBoxLayout* vl = new QVBoxLayout( w );
    QHBoxLayout* hl = new QHBoxLayout;
    vl->addLayout( hl );
    vl->addWidget( m_tabs = new QTestTabWidget );

    for (int i=0; i<3; i++)
    {
        const QString num = QString::number(i+1);
        QCheckBox* ch = new QCheckBox( QString("Show tab#")+num );
        ch->setChecked(true);
        hl->addWidget(ch);
        m_map.insert( ch, m_tabs->addTab(new QLabel( QString("Label ")+num ), QString("tab#")+num ) );
        // ^ QMap<QCheckBox*,int> m_map;
        connect( ch, SIGNAL(stateChanged(int)), this, SLOT(stateChanged(int)) );
    }

    setCentralWidget( w );
}

void MainWindow::stateChanged( int state )
{
    QCheckBox *p = qobject_cast<QCheckBox*>( sender() );
    if ( p==NULL||!m_map.contains(p) )
        return;

    m_tabs->setTabEnabled( m_map[p], state!=Qt::Unchecked );
    m_tabs->setStyleSheet();
}

PS Наткнулся на одну "кривость": если использовать виджеты на вкладках (в моём случае это раскрывающийся список на том месте, где обычно кнопка закрытия вкладки):
QComboBox *ysel = new YearInTabComboBox( years ); // наследник QComboBox
tabBar()->setTabButton(num, QTabBar::RightSide, ysel); // установка виджета во вкладку num справа от текста
то при сокрытии вкладки этим методом эти виджеты у меня должным образом не скрываются и могут образовать "стопку" уложившись рядом. (С кнопками закрытия, наверное, будет то же самое). Впрочем, на сколько я представляю, во-первых, мало кто ставит виджеты во вкладки, а во-вторых это можно обойти, дополнительно скрывая и показывая такие виджеты на соответствующих вкладках обычным способом (методами show/hide).
forest, summer

Размер медленной выборки и «последняя страница» (MySQL - PHP - браузер/клиент)

Рассмотрим простую страндартную задачу: данные из таблицы MySQL через PHP направляются в браузер (или иную программу-клиент). В браузере они отображаются в виде таблицы разбитой на «страницы» по 20, 50 или 100 записей на странице. То есть если записей много, пользователь видит только первые, скажем, двадцать, а ниже таблицы переключатель страниц: 1,2,3,..., последняя, называемый «paginator».

Эта задача весьма «заезжанная» и в ней нет ничего сложного, если трудоёмкость выборки из СУБД не большая. В этом случае как правило скрипты PHP принимают аргументом номер показываемой «страницы» N (по умолчанию - N=1) и размер страницы M из фиксированномго множества(20,50,100, а по умолчанию, скажем, M=20) сначала делают запрос SELECT COUNT(*) …. из таблицы с условиями выборки. Полученый размер выборки T делят на M, получая количество страниц C=M/T (приводя до целого числа в большую сторону), проверяют что N<=C, затем делают выборку записей для текущей «страницы» с помощью ...LIMIT (N-1)*M,M
Однако если таблица в MySQL очень большая полная выборка или подсчёт записей в выборке COUNT(*) может затянуться на продолжительное время. При этом, выдача полезных результатов в текущую страницу (особенно в первую) может происходить удовлетворительно быстро:
При не-буверизированном чтении выборки из MySQL записи могут приходить, например, каждые несколько десятков миллисекунд. Таким образом первая «страница», может быть загружена за приемлемое время порядка секунды, но если в выборке сотни тысяч записей и более, запрос COUNT(*), считающий записи, сам по себе может выполняться минуту и более!
Конечно, большого смысла показывать пользователю более нескольких тысяч записей нет. В случае если выборка очень большая, пользователю нужно увидеть просто фрагмент данных, что бы он мог на глаз проверить, что это вообще то что нужно.
Но иногда заказчик хочет, что бы пользователь всё же мог: (a) узнать общее количество записей в выборке; (b) перейти для просмотра на последнюю страницу выбоки.
Эти условия на мой взгляд, являются вполне обоснованными, т.к. могут реально помочь пользователю соориентироваться в его работе.
Мы не можем гарантировать выдачу результата за приемлемое для интерактивного взаимодействия «запрос-ответ» время, но можем показывать пользователю динамическую оценку результата требования (a).
Я имею в виду следующее: получив в течении секунд после запроса первую «страницу» и переключатель страниц: на 1-10 страницы, пользователь наблюдает бегущее увеличивающееся значение количества записей в выборке: «найденно не менее 200 записей...», «не менее 300...», «не менее 400...» итд. По мере этого может соответствующим образом расти и переключатель страниц. Пока не увидет окончательный результат «в выборке 12345 записей!», и появится возможность переключиться на последнюю «страницу».
Как это сделать?
Ни логика реляционных СУБД (в данном случае MySQL) ни логика WWW и HTTP не была изначально расчитана на такую работу.
MySQL не может информировать о состоянии счётчика COUNT(*), до тех пор, пока не подсчёт не будет полностью завершен, то есть задав такой запрос мы должны ждать пока он не выполнится, не дождавшись полного подсчёта мы не можем получить ответ на вопрос «ну а есть ли там хотя бы 100 записей?».
HTTP не был преспособлен для установки постоянных соединений, так как это делают TCP-socket-ы. Однако тут всё же проще: есть что называется «хак», можно просто не закрывать соединение после отработки основной части PHP (предварительно отключив тайм-аут выполнения скриптов!).
После завершающего </HTML> браузер продолжает воспринимать комманды вызова JavaScript-функции с данными в аргументах вида:
<SCRIPT>counter(200);</SCRIPT>
<SCRIPT>counter(300);</SCRIPT>
…
<SCRIPT>total(12345);</SCRIPT>
Естественно, заранее надо создать и загрузить в блоке HEAD эти JavaScript фунцкии counter(...) и total(...), которые будут утилизировать динамически поступающие значения (в данном случае текущая оценка размера выборки) должным образом (показывать их пользователю в должном месте, перестраивать переключатель «страниц» итд). Это не совсем «по-правилам», но это работает. После каждой такой посылки приходится посылать килобайтик пробелов и переводов строки (не считая уж комманды flush(); в PHP), т.к. полностью отключить кеширование по всей цепочке от серверного скрипта до обработчика JavaScript в браузерах не возможно.
При работе с MySQL, ради подсчёта COUNT(*) остаётся только после посылки нужных записей продолжать читать ненужные записи в холостую, ни куда не отправляя результаты, а только пересчитывая их в цикле и вызывая через каждые, скажем, 100 посчитанных записей вывод новой порции тегов «SCRIPT», а в конце -
<SCRIPT>total(...);</SCRIPT> 


В моей работе данные отправлялись не в браузер, а в специальную программу-клиент в формате XML, где разбирались по мере поступления SAX-парсером. С XML такой подход выглядит более элегантно. Я сразу грузил данные для всех «страниц», при их переключении нового запроса к серверу не создавалось. Мой более сложный алгорим всего этого таков:
Запускается небуферезированный цикл чтения выборки. Записи выдаются в клиент, в атрибуте тега XML записи указывается порядковый номер записи. Программма-клинет показывает пользователю записи по мере поступления, и он сразу может с ними работать.
Когда выведено заданное предельное кол-во страниц P, то есть M*P записей, они вместовыдачи в клиент начинают попадать в FIFO буфер $dlist = new SplDoublyLinkedList(); // Класс SplDoublyLinkedList из SPL есть в PHP начинаяя с версии 5.3.0
Этот буфер растёт до предела равного максимальному возможному размеру страницы M:

    $dlist
->push($r); // $r - новый элемент XML с очередной записью из выборки
    
if ($dlsize>=$dlmax// буфер заполнен
    
{
      
$dlist->shift();
    }
    else
    {
      
$dlsize++; // $dlsize это размер буфера
    
}
    
Как только выборка завершилась мы выдаём содержимое этого бувера по HTTP клиенту. В моём коде это выглядит так:
 
      fwrite
$f"<lastblock size=\"$dlsize\">\n" );
      
$dlist->setIteratorMode(SplDoublyLinkedList::IT_MODE_FIFO);
      for (
$dlist->rewind(); $dlist->valid(); $dlist->next()) {
        
fwrite$f$dlist->current() );
      }
      
fwrite$f"</lastblock>\n" );
    
Клиент может понять, что это последняя «страница», во-первых, по тегу lastblock либо, во-вторых, по разрыву в порядковых нумерах записей выборки.
Если же по истечению некоторого времени после начала «чистого счёта» или например, после пересчёта заданного количества записей «конца счёту не видно», запускается второй SQL-запрос COUNT(*), выполнение которого происходит параллельно. Т.к. в первом запросе используется не буфферезированное чтение обязательно надо делать второе соединение с MySQL. Хотя второй одновременно работающий в СУБД запрос тормозит первый, такой подход всё же показывает в среднем выйгрыш в скорости обслуживания.
Эти два условных «потока» выполнения начинают работать параллельно, и информация об общем колличестве записей и о содержании последней «страницы» в конце концов берётся из того потока, который выполнился быстрее (первый по любому поставляет «бегущую» информацию о размере выборки, не позволяя пользователя соскучиться).
В ходе второго «потока» после отработки первого SQL-зароса COUNT(*) запускается второй SQL-запрос с LIMIT-ом для получения записей последней «страницы». Отслеживание его выполнения происходит в цикле чтения основного первого «потока» с помощью упомянутой мной в предыдущем посте конструкции

        
// request with LIMIT is already sent
        
$ready $error $reject = array($mysqli2); // $mysqli2 — объект второго соединения.
        // или может так?: $ready[] = $error[] = $reject[] = $mysqli2;
        
mysqli_poll$ready,$error,$reject0,50000); // wait 1/20 sec
        
if (count($ready)>0)
        { 

    
После того как любой из двух условных потоков завершился происходит корректное закрытие XML, завершение PHP и передачи, и программа-клиент информаирует о том что приём данных, наконец, полностью завершён.
forest, summer

Неблокирующий запрос к MySQL из PHP

Казалось бы – что можно сказать нового к такой заезженной и тривиальной теме: доступ к данным в MySQL по запросу через браузер – Apache – PHP?
Оказывается и в такой казалось бы банальной операции можно встретить проблемы, которые мало кто знает как решать.

В задачах, с которыми столкнулся я, запросы к СУБД штатно могут быть столь тяжёлыми, что выполнятся продолжительное время. Заметные задержки при этом оказались неизбежны – даже на самых быстрых серверах с применением рейдов-0 из SSD под данные СУБД. Существенные задержки позволили появиться возможности и проблеме: пользователь может захотеть прервать выполнение запроса, передумать ждать результатов. Иными словами запрос может стать не актуальным, и тогда его надо корректно прервать. То же самое может произойти из-за аварийного обрыва соединения или из-за тайм-аута.

Реализация таких отмен запросов, может потребоваться и в простом web-интерфейсе, и при запросах данных через AJAX, и при взаимодействии с сервером специальных клиентских программами по HTTP. С серверной стороны, в принципе, подобная задача может возникнуть и при получении данных из других СУБД или вообще из других медленных источников. В этой статье я привожу решение, относящееся к извлечению данных именно из MySQL через PHP под Apache2 используя новый программный интерфейс MySQLi, то это относится к множеству вариантов клиентов, но к совершенно конкретному получению данных на сервере.

Знакомство вопросом реализации обработчиков отмены или обрыва сразу выявило принципиальное несовершенство связки PHP-Apache2: PHP-скрипт в принципе не может обнаружить обрыв или даже штатное закрытие соединения до тех пор пока не будет ничего отправлять клиенту в сеть. Причём по не регламентировано сколько данных для этого надо отправить, в указаниях все вроде соглашаются с тем что одного байта вроде бы будет не достаточно, часто отправляют холостые блоки из целого килобайта нулей или пробелов. Возможно, это зависит от конкретного сервера, версии PHP и их настроек. Экспериментально я определил, что на моём сервере можно отправлять 32 перевода строки.

В связке PHP-MySQL тоже оказался существенный изъян: все простые способы чтения данных оказались блокирующими, т.е. они приостанавливают работу PHP скрипта до тех пор, пока MySQL не вернёт их (либо не случится тайм-аут). И соответственно, в это время скрипт ничего послать в сеть не может для того, что бы проверить соединение с HTTP-клиентом. Да, можно применить небуфиризированное чтение, но в моём случае задержки не были связанны с большим количеством записей в выборках; даже извлечение одного единственного значения из СУБД может приостановить ход выполнения PHP. В случае отмены можно, конечно, просто оборвать HTTP-соединение с клиента. Но при этом не только бесполезно перенагружается сервер, который продолжает обрабатывать ненужный “зомби-запрос”, но если вы применяете сессии, а СУБД “уйдёт в себя”, ненароком может произойти глобальная блокировка не только окна браузера с запросом, но и всего сайта, да так, что придётся перегружать всё сбрасывая сессию. Блокировка сессии может случится, если на момент обращения к СУБД, которое затянулось, сессия не была закрыта на запись.

Существует одно универсальное решение – при постановке запроса в MySQL получить номер MySQL-процесса, передать его в клиент. Тот если надо, может его использовать, если понадобится, передав по другому соединению в специальный скрипт на сервере, который “собъёт” ставший не нужным запрос с помощью SQL-зароса-комманды KILL, что в свою очередь приведёт и к разблокировки PHP, обрабатывающего запрос. Этот способ, без сомнения, применим, однако он сложен, некрасив и небезопасен. При его реализации нужно опять-таки иметь в виду опасность блокировок сессии, о которой я упоминал выше.

Мне пришло в голову использовать в PHP параллельный поток (thread) для проверки наличия соединения по HTTP, приблизительно так:
class Ping0 extends Thread {
  public function 
run() {
    
//if (ob_get_level()) ob_end_clean(); 
    
sleep(1);
    echo 
str_repeat("\n",32);
    
flush();    // Error 6 (net::ERR_FILE_NOT_FOUND): The file or directory could not be found.
    
ob_flush();
  }
}

function 
db_query_long($qstring,$conn
{
  
ignore_user_abort(false);
  
//if (ob_get_level()) ob_end_clean(); 

  
$ping0 = new Ping0();
  
$ping0->start(); 
  
  
$ret db_query($qstring,$conn);

  
// $ping0->stop();

  
return $ret;
}
Но этот способ не заработал, скрипт падал с неадекватными ошибками на flush(), создающими впечатление багов в PHP. Встроенный класс потоков Thread в PHP произвёл впечатление очень “сырого”, его даже не было в стандартной инсталляции и что бы воспользоваться требовалось пересобирать PHP из исходников с поддержкой ZTS (Zend Thread Safety) (опции --enable-maintainer-zts или --enable-zts в Windows). В общем, я решил оставить этот путь, хотя его преимущество в том, что он мог бы помочь в решении задачи обработчика отмены запросов не только из MySQL но и из других медленных источников данных.
К счастью, в MySQLi есть нетривиальный способ обработки запроса к БД, который позволил мне благополучно совершить неблокирующее чтение непосредственно.
Итак к вашему вниманию вот фрагмент функции, организующий неблокирующей запрос и возвращающей результат в виде объекта mysqli_result :
    $r $gl_mysqli1->query($sqlMYSQLI_ASYNC ); 

    
ob_implicit_flush(true);
    
ignore_user_abort(false); // можно поставить true

    
for ($i=0$i<$gl_tout$i++) // $gl_tout - тайм-аут; максимальное время в секундах
    
{
      
$ready $error $reject = array($gl_mysqli1);
      
// $ready[] = $error[] = $reject[] = $gl_mysqli1;

      
mysqli_poll$ready,$error,$reject1); // ждём 1 cек; можно воспользоваться пятым параметром - микросекунды
      
if (count($ready)>0)
      {
      
// ready - данные получены
      
$r $gl_mysqli1->reap_async_query();
      if (
$r
      {
        
// успех - данные получены
        
return $r;
      }
      
// some error ??
      
return $r;
      }
      if ( 
count($error)>|| count($reject)>)
      {
      
// какая-то ошибка - error
      
trigger_error("(" $gl_mysqli1->connect_errno ") "
        
$gl_mysqli1->connect_errorE_USER_ERROR);

      return 
null;
      }

      
// проверка соединения с клиентом по HTTP - test connection
      
echo str_repeat("\n",32); // посылка нулей приводит к ошибкам и глюкам
      
flush();
      
ob_flush();

      if (
connection_status()!=CONNECTION_NORMAL)
      {
       
// соединение с клиентом оборволось, запрос на СУБД не актуален
        
return null;
      }
  
      
// возможно нормальное состояние — данные ещё не готовы, надо подождать
    
}
    
// если мы тут - время вышло - таймайт $gl_tout сек.

Метод mysqli::poll до сих пор весьма плохо документирован...
Примечание: лишние переводы строки между управляющими тегами HTML и XML обычно никак не проявляются.
forest, summer

Компьютерное: бесплатные программные словари

Несколько слов про мир открытого и бесплатного софта, который медленно но верно с каждым годом становится мне всё ближе и роднее.

Расскажу вкратце про бесплатные программы-словари – аналоги хорошо многим известного ABBYY Lingvo. Все програмки, из попавшихся мне под руку использовали отдельные подключаемые словари - в сети (что мне не нужно) или локаьные, в виде файлов (что мне нужно). Это с одной стороны удобно, потому что делает их гибкими и “разносторонними”. (Вот я, например, пользуюсь не только английским и русским языками. А ещё словарь может быть и токовым, по определённой теме). С другой стороны всё усложняет – кроме бесплатной программы надо ещё где-то разыскать нужный бесплатный файл-словарь, да ещё именно такой, что бы он подошёл к программе. В общем, обычный в мире Linux-софта подход – всё для желающих вдоволь “поковыряться”.
Итак, обнаружено несколько программ-оболочек; наиболее продвинувшиеся (как мне кажется):
OpenDict + [можно скачать тут].
StarDict + [ещё ссылка]
FreeDict
Это свободный софт! :-) Из всего этого зоопарка у меня на ноутбуке под EEEBuntu (это там такой Linux) с ходу нормально заработал только QStarDict - вариант StarDict-a, сделанный с использованием библиотеки Qt4.

Теперь где скачать открытые словари (эту ссылку очень непросто найти поисковиками!):
http://xdxf.revdanica.com/down
OpenDict использует формат словарей XDXF, а StarDict – свой формат (dict). Где-то в сети лежит программка-конвертер из одного типа в другой, но с этого сайта можно скачать словарь нужного типа.

Скачал версию программы под Windows: OpenDict-0.6.2-win32bin.zip
(он мне больше всего приглянулся). Яко бы, он понимает форматы и OpenDict plugin/zip и StarDict (распространённые, с сайта) и ещё форматы Slowo (расширение swa) и Mova :-)
Последнее и спасло. Ибо ни один словарь “родных” форматов, скаченный с вышеуказанного сайта, не захотел работать!
Это freeware. Комерческое ПО, впрочем, бывает и не лучше. В общем, нашёл коллекцию словарей в формате Mova:
http://sourceforge.net/projects/xdxf/files/dicts-mova/001/
Скачал и успешно установил в OpenDict-0.6.2 под мой Windows 2000 sp4; скачал и зарегистрировал словари (каждый словарь – это пара файлов .index и .dict.dz):
mova01_magus и mova01_mueller7 (англо-русские)
mova01_slovnyk_ru-en (русско-английский). Некоторые слова он переводит как то странно: “пример” => “witness” :-)

Кстати, вспомнилось: когда-то у меня был клавиатурный КПК Windows CE “Jornada”. Там, в качестве такого словаря, я использовал особым образом запускаемую программу HaaliReader.

PS См. также: http://opennet.ru/prog/sml/145.shtml