среда, 6 июня 2018 г.

Несколько мыслей об HTA

  1. Об HTML-приложениях.
  2. Краткое сравнение или почему HTA выглядит привлекательно
  3. В поисках утраченного диалога
  4. Не оставлять же все в таком виде
  5. А нельзя ли сделать поудобнее?
  6. Попробуем что-нибудь запустить
  7. Заключение

Об HTML-приложениях.



Клиентские Web-технологии (я сейчас говорю в первую очередь о связке HTM/JavaScript/CSS) уже давно вышли за пределы той ниши, которая была им отведена изначально. Здесь можно вспомнить и ряд форматов электронных книг и справки, основанных на этих технологиях и язык JavaScript, когда в microsoft появилась собственная его реализация(JScript), язык стал использоваться для написания административных скриптов. На самом деле это все вполне закономерный процесс. Языки просты для освоения, приложения, написанные на них обладают достаточной гибкостью и возможностью создавать мощные интерфейсы, и самое главное - благодаря вебу их хотя бы на базовом уровне знает любой программист. Поэтому конечно, появление технологий, позволяющих писать полноценные настольные приложения с использованием этих языков - в каком-то смысле было неизбежным.

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

Расширения для различных программ. Из того, что я видел - это расширения для программы Microsoft Expression Web. В нем можно создавать панельки на HTML/JavaScript/CSS и встраивать их в программу в виде расширений. Аналогичная возможность появилась в Microsoft Office, если не ошибаюсь с версии 2013-го года.

Гаджеты для рабочего стола операционных систем Windos Vista и Windows 7. Технология умерла вместе с этими операционными системами, но тем не менее...

Приложения магазина Windows. Их можно писать не только на HTML, но это сути дела не меняет.

Платформа Mozilla в виде проекта XulRunner. Конечно там используется не HTML, по крайней мере не HTML в чистом виде, но в основе все равно лежат веб-технологии. XUL - это формат построенный XML, его элементы помимо собственных API поддерживают основные интерфейсы элементов HTML, поэтому могут быть обработаны тем же способом. Ну и кроме того, в XUL-документе можно использовать HTML-элементы в любом соотношении, причем как прямо в документе, так и в элементе browser или, например, во фрейме. Во всех случаях есть доступ к этим элементам. Также там используются CSS и JavaScript. Из-за этого всего я все-таки думаю, что упомянуть его тут было вполне уместно.

Следующие два проекта упомяну вместе, в силу их сходства и общих средств, которые они используют. Это проекты Electron и NW.js (бывший node-webkit). Объединяет то, что оба они построены на Node.js и браузерном движке WebKit.

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

Кроме описанных вариантов написания HTML-приложений не лишне было бы упомянуть и о проектах ориентированных не только на десктоп. Для мобильных приложений существует проект PhoneGap, в котором веб-движок используется ради кроссплатформенности, то есть проект написанный под PhoneGap может быть собран под разные мобильные платформы. А если говорить о Smart-TV, то, насколько мне известно, приложения для всех Smart-TV-платформ пишутся с использованием веб-технологий.

Краткое сравнение или почему HTA выглядит привлекательно



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

Среди бесспорных достоинств XullRunner, Electron и NW.js на первом месте, конечно же, кроссплатформенность. Кроме этого следует отметить обширные и прекрасно задокументированные API. У Mozilla вся документация на сайте MDN, для Electron и NW.js актуальна документация по Node.js и его модулям плюс сайты самих проектов. Если говорить о XulRunner, то тут еще можно отметить, что XUL - прекрасная GUI-платформа и там не надо "мастерить" элементы управления из всего, что под руку попадется и кроме того там много всяких фишек, типа биндингов, обсерверов, броудкастеров, оверлеев и кучи всяких "вкусняшек".

В противовес всему вышесказанному HTA выглядит ну ооооочень скромно. Кроссплатформенности нет. Из встроенных API только элемент HTA:APPLICATION, определяющий некоторые параметры окна, атрибут application для фреймов, позволяющий определить контекст безопасности фрейма, ну и ActiveXObject. Вся программная часть, отличная от того, что делается на странице, реализуется через элементы ActiveX. Документация по ним скудна и разрознена. Ну и тут, естественно, возникает вопрос: что же привлекло мое внимание именно в этой технологии.

Так вот, если разобраться, то чего мы ждем от HTML-приложения? Ну наверное, чтобы можно было написать простой код на HTML, скрипт на JavaScript, сохранить, запустить и чтобы это заработало. Ну вот что-то типа такого.
Код html5 Выделить
<button onclick="alert('Привет, кнопкожмун.');">Нажми меня.</button>
Отличительной особенностью HTA является как раз то, что если сохранить текст с таким содержимым в файл с расширением .hta и запустить его, то произойдет именно то, что описано выше (в Windows 10 у меня при первом запуске такого приложения сначала открылось окошко выбора приложения, с помощью которого надо открыть файл. В таком случае надо выбрать HTML приложение и не забыть установить флажок, чтобы эти файлы всегда открывались именно так). При этом все приложение "весит" ровно столько, сколько потребовалось текста на его написание.

В противовес этому для приложений Mozilla нужен XulRunner, последняя версия, которую я видел "весила" 75MB и его нужно "таскать" за приложением. Кроме того, для создания приложения надо: правильно организовать каталоги, написать манифест, написать application.ini, написать prefs.js. Запуск приложения - тоже отдельный вопрос. Приложения, использующие Node.js требуют написания package.json и если NW.js еще запускается как приложение главного окна, то в Elctron сначала запускается скрипт, в котором помимо всего прочего надо еще создать и запустить главное окно. Развертывание этих приложений - тоже морока. Electron требует разместить приложение в папку с самим "электроном" в определенном месте и запускать оттуда. Распространять тоже придется вместе с ним. Для Windows 64 bit скачал я этот пакет, распаковал - "весит" 140MB. Думаю с NW.js дело обстоит не лучше, поскольку значительная его часть состоит из Node.js и WebKit. Конечно, там есть варианты, но как по мне - так оно того не стоит, а потому есть смысл рассмотреть возможности именно HTA.

В поисках утраченного диалога



Одной из важнейших частей пользовательского интерфейса являются диалоговые окна. Сложно себе представить, например, полноценную работу с файловой системой при отсутствии диалогов открытия и сохранения файлов. Какого-то конкретного места на MSDN, где были бы собраны описания всех элементов ActiveX, которые могли бы понадобиться для HTA-приложений, насколько я знаю, не существует, поэтому все приходится искать. В свое время (еще во времена Windows XP) я нашел один компонент для работы с файл-диалогами, он назывался UserAccounts.CommonDialog. Но давно к этой теме не возвращался и сейчас вдруг выяснил, что он уже давно недоступен. Был еще какой-то, не помню уже названия, но в процессе поисков обнаружил, что и он тоже недоступен. Тогда я начал искать решение этой проблемы и наткнулся вот на такой скрипт.
Кликните здесь для просмотра всего текста
Код vb Выделить
Function ChooseFile (ByVal initialDir)
 
  Set shell = CreateObject("WScript.Shell")
 
  Set fso = CreateObject("Scripting.FileSystemObject")
 
  tempDir = shell.ExpandEnvironmentStrings("%TEMP%")
 
  tempFile = tempDir & "\" & fso.GetTempName
 
  ' temporary powershell script file to be invoked
  powershellFile = tempFile & ".ps1"
 
  ' temporary file to store standard output from command
  powershellOutputFile = tempFile & ".txt"
 
  'input script
  psScript = psScript & "[System.Reflection.Assembly]::LoadWithPartialName(""System.windows.forms"") | Out-Null" & vbCRLF
  psScript = psScript & "$dlg = New-Object System.Windows.Forms.OpenFileDialog" & vbCRLF
  psScript = psScript & "$dlg.initialDirectory = """ &initialDir & """" & vbCRLF
  psScript = psScript & "$dlg.filter = ""ZIP files|*.zip|Text Documents|*.txt|Shell Scripts|*.*sh|All Files|*.*""" & vbCRLF
  ' filter index 4 would show all files by default
  ' filter index 1 would should zip files by default
  psScript = psScript & "$dlg.FilterIndex = 4" & vbCRLF
  psScript = psScript & "$dlg.Title = ""Select a file to upload""" & vbCRLF
  psScript = psScript & "$dlg.ShowHelp = $True" & vbCRLF
  psScript = psScript & "$dlg.ShowDialog() | Out-Null" & vbCRLF
  psScript = psScript & "Set-Content """ &powershellOutputFile & """ $dlg.FileName" & vbCRLF
  'MsgBox psScript
 
  Set textFile = fso.CreateTextFile(powershellFile, True)
  textFile.WriteLine(psScript)
  textFile.Close
  Set textFile = Nothing
 
  ' objShell.Run (strCommand, [intWindowStyle], [bWaitOnReturn])
  ' 0 Hide the window and activate another window.
  ' bWaitOnReturn set to TRUE - indicating script should wait for the program
  ' to finish executing before continuing to the next statement
 
  Dim appCmd
  appCmd = "powershell -ExecutionPolicy unrestricted &'" & powershellFile & "'"
  'MsgBox appCmd
  shell.Run appCmd, 0, TRUE
 
  ' open file for reading, do not create if missing, using system default format
  Set textFile = fso.OpenTextFile(powershellOutputFile, 1, 0, -2)
  ChooseFile = textFile.ReadLine
  textFile.Close
  Set textFile = Nothing
  fso.DeleteFile(powershellFile)
  fso.DeleteFile(powershellOutputFile)
End Function

Выглядит жутковато, но, не найдя больше ничего рабочего, я все-таки решил его опробовать.
Код html5 Выделить
<button onclick="div1.innerHTML=ChooseFile('c:')">Выбрать файл</button>
<div id="div1"></div>
<script type="text/vbscript">
 
</script>
В элемент script вставляем эту функцию, сохраняем с расширением .hta, запускаем, жмем кнопку, появляется диалог, выбираем файл, имя файла появится на экране после закрытия диалога. Таким образом все прекрасно работает, а стало быть есть смысл изучить код подробнее.

Вначале создается два ActiveX объекта Wscript.Shell и Scripting.FileSystemObject. Первый для запуска внешнего приложения, второй для работы с файловой системой. Далее генерируются два имени временных файлов. После чего формируется текст скрипта PowerShell, в котором, собственно, вся магия и происходит. Затем скрипт сохраняется во временный файл, запускается, результат работы скрипт сохраняет во второй временный файл. По завершении работы скрипта из временного файла №2 считывается результат, временные файлы удаляются и результат возвращается функцией.

Файл-диалог, конечно, вещь полезная, но тут наибольший интерес вызывает не это, а то, что предложен простой и способ получить из приложения HTA доступ ко всей мощи PowerShell, а через нее и к .Net Framework.

Не оставлять же все в таком виде



Приведенная выше фукнция хоть и прекрасно работает, тем не менее имеет как минимум два недостатка: написана на VBScript и не универсальна. VBScript, конечно, поддерживается браузерным движком, который используется в приложении, но только в режиме совместимости с прежними версиями. То есть, если мы возьмем тот же код и вставим его в полный документ, да еще добавим в него мета-тэг, указывающий версию IE=9 или выше, то это работать не будет.
Код html5 Выделить
<meta http-equiv="X-UA-Compatible" content="IE=11" />
А между тем, хотелось бы использовать новые возможности всех языков HTML/JavaScript/CSS. Поэтому, конечно, лучше было бы перевести код на JavaScript. Что до универсальности, то тут можно вывести код PowerShell-скрипта в параметры функции и все остальное можно и не менять, а с этой фукнцией запускать любые скрипты. Примерно вот так
Код javascript Выделить
        function RunPS(psScript, args)
        {
            var tempDir = shell.ExpandEnvironmentStrings("%TEMP%");
            var tempFile = tempDir + "\\" + fso.GetTempName();
            var powershellFile = tempFile + ".ps1";
            var powershellOutputFile = tempFile + ".txt";
            var argStr = " ";
            for (var a in args)
            {
                argStr += "-" + a + " \"" + args[a] + "\" ";
            }
            var textFile = fso.CreateTextFile(powershellFile, true);
            textFile.WriteLine(psScript);
            textFile.Close()
            textFile = null;
            var appCmd = "powershell -ExecutionPolicy unrestricted -file \"" + powershellFile + "\" -output \"" + powershellOutputFile + "\" " + argStr;
            div1.innerText = appCmd;
            shell.Run(appCmd, 0, true);
            textFile = fso.OpenTextFile(powershellOutputFile, 1, 0, -2);
            var result = textFile.ReadLine();
            textFile.Close();
            fso.DeleteFile(powershellFile);
            fso.DeleteFile(powershellOutputFile);
            return result;
        }
Данные в скрипт можно передавать через аргументы, которые будут считываться в ps-скрипте, или добавлять к нему текст с переменными, которые потом в нем будут читаться. Сам скрипт при этом можно будет хранить где-то в скрытом HTML-коде приложения, чтобы не мучиться с конкатенациями строк и т. д.
Код html5 Выделить
    <div id="ps-scripts" style="display: none; white-space: pre;">
        <pre id="openFileDialogScript">
            Param(
[string]$initialDir,
[string]$title,
[string]$output
)
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null;
$dlg = New-Object System.Windows.Forms.OpenFileDialog;
$dlg.initialDirectory = $initialDir;
$dlg.Title = $title;
$dlg.ShowHelp = $True;
$dlg.ShowDialog() | Out-Null;
Set-Content $output $dlg.FileName; 
</pre>
    </div>
Ну, а вызов такого скрипта будет выглядеть как-то так
Код javascript Выделить
        function openFileDialog()
        {
            return RunPS(openFileDialogScript.innerHTML, {
                "initialDir": "c:\\Users\\diadiavova\\",
                "title": "Выбрать файл"
            });
        }
Естественно, можно добавить и другие скрипты и они будут вызываться примерно так же.

А нельзя ли сделать поудобнее?



Все это уже выглядит неплохо, но если пользоваться этим постоянно, то, по всей видимости лучше сделать еще лучше. Для начала надо уяснить, что автор той функции на VBScript пытался внести в нее полную функциональность, необходимую для запуска диалога, а перед нами такой задачи не стоит, поэтому не смысла вклинивать код PowerShell в код самого приложения. Мы можем держать ps1-файла в каталоге приложения или в отдельном каталоге и просто вызывать. Как минимум мы таким образом избавимся от необходимости постоянно сохранять их во временные файлы и удалять. Кроме того это разгрузит код приложения и сделает доступными скрипты для различных окон.

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

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

Ну и ActiveX-объекты многоразового использования также можно создать один раз и пользоваться.

Исходя из этих соображений у меня получилась вот такая небольшая библиотека
Кликните здесь для просмотра всего текста
Код javascript Выделить
var hta =
    {
        file:
            {
                _fso: new ActiveXObject("Scripting.FileSystemObject"),
                _stream: new ActiveXObject("ADODB.Stream"),
                _getStream: function ()
                {
                    if (this._stream.State != 0)
                    {
                        this._stream.Close();
                    }
                    return this._stream;
                },
                fileExists: function (fileName)
                {
                    /// <summary>Проверяет, существует ли файл и возвращает true, если существует и false в обратном случае</summary>
                    /// <param name="fileName" type="String">Путь к файлу, существование которого надо проверить</param>
                    /// <returns type="Boolean" />
                    return this._fso.FileExists(fileName);
                },
                deleteFile: function (fileName)
                {
                    /// <summary>Удаляет файл</summary>
                    /// <param name="fileName" type="String">Путь к файлу, который надо удалить</param>
                    this._fso.DeleteFile(fileName);
                },
                readText: function (fileName, encoding)
                {
                    /// <summary>Считывает текст из временного файла и возвращает считанный текст</summary>
                    /// <param name="fullFileName" type="String">Абсолютное или относительное имя файла, из которого считывается текст. Относительный путь будет вычисляться отосительно текущего каталога приложения.</param>
                    /// <param name="encoding" type="String">Кодировка текста</param>
                    /// <returns type="String" />
                    var stream = this._getStream();
                    try
                    {
                        stream.Charset = encoding || "utf-8";
                        stream.Open
                        stream.LoadFromFile(fileName);
                        var result = stream.ReadText(-1);
                    } catch (e)
                    {
                        throw (e);
                    }
                    finally
                    {
                        stream.Close();
                    }
                    return result;
                },
 
                readDeleteText: function (fullFilename, encoding)
                {
                    /// <summary>Считывает текст из временного файла, после чего удаляет этот файл и возвращает считанный текст</summary>
                    /// <param name="fullFileName" type="String">Абсолютное имя файла, из которого считывается текст</param>
                    /// <param name="encoding" type="String">Кодировка текста</param>
                    /// <returns type="String" />
                    var result = this.readText(fullFilename, encoding);
                    this.deleteFile(fullFilename);
                    return result;
                },
 
                currentDir: function ()
                {
                    /// <summary>Возвращает путь к текущей дирректории</summary>
                    /// <returns type="String" />
                    return this._fso.GetAbsolutePathName(".");
                },
 
                getTempFileName: function ()
                {
                    /// <summary>Создает и возвращает имя временного файла в дирректории %TEMP% (самого файла при этом еще не существует)</summary>
                    return this._fso.GetSpecialFolder(2) + "\\" + this._fso.GetTempName();
                },
 
                writeText: function (fileName, content, encoding)
                {
                    /// <summary>Записывает текст в файл</summary>
                    /// <param name="fileName" type="String">Имя файла для записи текста</param>
                    /// <param name="content" type="String">Текст для записи в файл</param>
                    /// <param name="encoding" type="String">Кодировка текста</param>
                    /// <returns type="String" />
                    var stream = this._getStream();
                    try
                    {
                        stream.Charset = encoding || "utf-8";
                        stream.Open();
                        stream.WriteText(content);
                        stream.SaveToFile(fileName, 2);
                    } catch (e)
                    {
                        throw (e);
                    }
                    finally
                    {
                        stream.Close();
                    }
 
                },
 
                writeTempText: function (content, encoding)
                {
                    /// <summary>Записывает текст во временный файл и возвращает путь к этому файлу</summary>
                    /// <param name="content" type="String">Текст для записи во временный файл</param>
                    /// <param name="encoding" type="String">Кодировка текста</param>
                    /// <returns type="String" />
                    var fileName = this.getTempFileName();
                    this.writeText(fileName, content, encoding);
                    return fileName;
                },
 
                writeTempObject: function (obj, encoding)
                {
                    /// <summary>Записывает сериализованный в JSON объект во временный файл и возвращает путь к этому файлу</summary>
                    /// <param name="obj" type="String">Объект для записи во временный файл</param>
                    /// <param name="encoding" type="String">Кодировка текста файла</param>
                    /// <returns type="String" />
                    return this.writeTempText(JSON.stringify(obj), encoding);
                }
            },
 
        powershell:
            {
                _shell: new ActiveXObject("WScript.Shell"),
                _runFiles: function (scriptFile, argFile, outputFileName)
                {
                    return this._shell.Run("powershell -ExecutionPolicy UNRESTRICTED -File \"" + scriptFile +
                        "\" -inputFile \"" + argFile + "\" -outputFile \"" + outputFileName + "\"",
                        0, true);
                },
 
                eval: function (scriptText, args)
                {
                    /// <summary>Исполняет скрипт, переданный в виде текста</summary>
                    /// <param name="scriptText" type="String">Текст скрипта, который нужно исполнить</param>
                    /// <param name="args" type="Object">Объект с аргументами скрипта</param>
                    /// <returns type="String" />
                    var psFileName = hta.file.writeTempText(scriptText);
                    var result = this.run(psFileName, args);
                    hta.file.deleteFile(psFileName);
                    return result;
                },
 
                run: function (fileName, args)
                {
                    /// <summary>Исполняет скрипт, переданный в виде имени файла</summary>
                    /// <param name="fileName" type="String">Имя файла скрипта, который нужно исполнить</param>
                    /// <param name="args" type="Object">Объект с аргументами скрипта</param>
                    /// <returns type="String" />
                    var argFileName = hta.file.writeTempObject(args);
                    var outputFN = hta.file.getTempFileName();
                    this._runFiles(fileName, argFileName, outputFN);
                    var result = hta.file.fileExists(outputFN) ? hta.file.readDeleteText(outputFN) : null;
                    hta.file.deleteFile(argFileName);
                    return result;
                }
            }
    };
Среди ActiveX-объектов, помимо упомянутых ранее, присутствует ADODB.Stream. Используется он для работы с текстовыми файлами. Это связано с тем, что FileSystemObject не очень "дружит" с кодировками при работе с текстовыми файлами.

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

Попробуем что-нибудь запустить



Для того, чтобы что-нибудь запустить, нам сначала понадобится то, что мы будем запускать, так что займемся написанием скрипта. Коль скоро начали мы с диалоговых окон, ими и займемся. Запишем вызов пяти основных дилоговых окон в один скрипт. При этом нам надо помнить следующее:
Запуск скриптов в нашей библиотеке осуществляется с двумя аргументами inputFile и outputFile. Первый будет содержать имя временного файла, из которого мы можем извлечь входные данные. Второй - имя файла, в который надо записать выходные данные. В качестве входных данных данных будет использоваться сериализованных в JSON объект, формат которого мы сейчас и определим, исходя из данных, которые нам понадобятся для работы скрипта.

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

Документация по всем окнам, нашего скрипта расположена по нижеследующим ссылкам:
Класс OpenFileDialog (System.Windows.Forms)
Класс SaveFileDialog (System.Windows.Forms)
Класс FolderBrowserDialog (System.Windows.Forms)
Класс ColorDialog (System.Windows.Forms)
Класс FontDialog (System.Windows.Forms)

Код скрипта
Кликните здесь для просмотра всего текста
dialogs.ps1
Код powershell Выделить
Param
(
[string]$inputFile,
[string]$outputFile
)
$input = Get-Content $inputFile -Encoding UTF8 | ConvertFrom-Json;
$props = @{}
$input.properties.psobject.properties |foreach {$props[$_.Name]=$_.Value};
[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null;
[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | Out-Null;
       
switch($input.dlgType)
{
    "OpenFile"
    {
        $dlg = New-Object System.Windows.Forms.OpenFileDialog -Property $props;
        $result = $dlg.ShowDialog().ToString();
        @{"DialogResult"=$result; "properties"=$dlg} | ConvertTo-Json | 
        Set-Content $outputFile -Encoding UTF8; 
    }
    "SaveFile"
    {
        $dlg = New-Object System.Windows.Forms.SaveFileDialog -Property $props;
        $result = $dlg.ShowDialog().ToString();
        @{"DialogResult"=$result; "properties"=$dlg} |ConvertTo-Json |
        Set-Content $outputFile -Encoding UTF8; 
    }
    
    "FolderBrowser"
    {
        $dlg = New-Object System.Windows.Forms.FolderBrowserDialog -Property $props;
        $result = $dlg.ShowDialog().ToString();
        @{"DialogResult"=$result; "properties"=$dlg} |ConvertTo-Json |
        Set-Content $outputFile -Encoding UTF8; 
    }
    
    "Font"
    {
        $dlg = New-Object System.Windows.Forms.FontDialog -Property $props;
        $result = $dlg.ShowDialog().ToString();
        @{"DialogResult"=$result; "properties"=$dlg} |ConvertTo-Json |
        Set-Content $outputFile -Encoding UTF8;
    }
    
    "Color"
    {
        $dlg = New-Object System.Windows.Forms.ColorDialog -Property $props;
        $result = $dlg.ShowDialog().ToString();
        @{"DialogResult"=$result;"HtmlColor"=[System.Drawing.ColorTranslator]::ToHtml($dlg.Color); "properties"=$dlg }| 
        ConvertTo-Json |
        Set-Content $outputFile -Encoding UTF8;
    }
}


В качестве параметра dlgType передается имя класса диалогового окна без слова Dialog на конце. Объект properties должен содержать правильные имена свойств, как они описаны в документации по классам окон и никаких других свойств там быть не должно, иначе это приведет к сбою. Следует помнить, что некоторые свойства предъявляют строгие требования к значению. Например, я как-то поначалу попробовал инициировать свойство InitialDirectory объекта OpenFileDialog значением c:\ и в результате не мог получить нормального ответа, поскольку путь надо указывать без слеша в конце. Ну и конечно, надо смотреть какие из свойств доступны только для чтения и их тоже не использовать.

Пример вызова OpenFileDialog
Код javascript Выделить
        function RunPS(shortName, args)
        {
            var path = hta.file.currentDir() + "\\powershell\\" + shortName + ".ps1";
            return hta.powershell.run(path, args)
        }
 
            var result = RunPS("dialogs", {
                dlgType: "OpenFile",
                properties:
                {
                    Title: "Выбрать текстовый файл",
                    Filter: "Текстовые файлы (*.txt)|*.txt|Все файлы (*.*)|*.*"
                }
            });
Здесь функцию RunPS принимает краткое имя файла, формирует из него полное имя, с учетом структуры каталога приложения. В данном случае у меня скрипты PowerShell расположены в папке powershell и функция вычисляет путь к этой папке, добавляет имя файла и расширение. После этого вызывает run из нашей библиотеки.

В качестве результата работы диалога скрипт вернет JSON, представляющий объект содержащий свойства DialogResult, это строка, имеющая одно из значений перечисления Перечисление DialogResult (System.Windows.Forms). Нас тут интересует, в основном, будет ли это свойство иметь значение OK. Также там будет свойство properties с полным набором свойств диалога после его работы, а стало быть любую информацию о результате работы диалога можно получить оттуда. Для ColorDialog объект результата будет иметь дополнительное свойство HtmlColor, Думаю, это актуально.

Скажу еще пару слов о коде самого скрипта. Любой скрипт, который будет работать с нашей библиотекой, должен иметь два параметра $inputFile и $outputFile. Первый - это адрес файла из которого должны считываться входные данные, второй - адрес файла, в который будет записываться результат работы (то, что в конечном итоге возвратит функция run). В данном случае набор свойств properties пришлось еще обойти в цикле и передать их в хэш-таблицу, с помощью которой инициировался объект диалога, поскольку то, что получается в результате работы ConvertFrom-Json непосредственно для этой цели не годится.

Полный пример с работой четырех окон из пяти (куда там всунуть FolderBrowserDialog, я просто не придумал) содержится во вложении к проекту. Приложение называется dialogs.hta.

Заключение



Описанный здесь подход, вряд ли следует использовать в любой ситуации. Достаточно попробовать с его помощью выполнить какую-нибудь простую операцию (например арифметическую), чтобы увидеть, что результата придется еще подождать, пусть это всего пара секунд, но все же это не очень хорошо для такой простой операции, того же диалогового окна иногда приходится секунд десять ждать. Оно и понятно: все эти операции с записью, чтением и удалением временных файлов; запуском внешнего приложения (powershell) и все такое, - вполне естественно занимают какое-то время. Тем не менее, если требуется дополнительный функционал, который невозможно получить, по крайней мере без установки на компьютер дополнительных компонентов, в таких случаях powershell оказывается мощным подспорьем. Да и задержки все эти как правило не так критичны. А вот возможности при этом увеличиваются существенно.

Во вложении помимо приложение dialogs.hta есть еще приложение testfunc.hta. В нем демонстрируется возможность создания библиотек функций на PoewrShell, которые можно импортировать в другие скрипты. Файл myfunc.ps1 содержит три функции:
  1. Первая конвертирует rtf-файл в xaml, ей над передать два аргумента - адрес rtf-файла и куда сохранять xaml.
  2. Вторая - выполняет XSLT-трансформацию. Принимает четыре аргумента. Первые два - адреса XML и XSLT документов. Третий (необязательный) - адрес, куда сохранять результат, если он не указан, к имени XML просто будет добавлен .output.xm. Четвертый - хэштаблица со списком параметров, которые можно передать самому преобразованию. В папке xml этого приложения пример XML документа, преобразования и результат. Помимо параметра в преобразовании есть еще скрипт на VB.Net как пример.
  3. Третья функция принимает ProgId ActiveX компонента и возвращает JSON с его свойствами и методами. Можно использовать как прототип при написании JavaScript-кода, использующего этот компонент. Написано опять-таки для примера, так что все имеет достаточно схематический вид.
Приложение, демонстрирующее функции, использует только последние две.
Вложения

Комментариев нет :

Отправить комментарий