3 декабря 2013 в 06:46

Кроссплатформенный GUI на C# и веб-технологии

Самая первая спецификация продукта, частично устная, содержала требование – наличие кроссплатформенного(Windows, Linux, Mac) клиента под десктоп и облегченную версию мобильного(Windows, Android, iPhone). По возможности интерфейс должен быть максимально похожим на разных ОС.
Благодаря Mono мы можем писать кроссплатформенные приложения, но вопрос с GUI остается открытым. Имеющиеся технологии под .Net(Windows Forms, WPF) хорошо работают только под Windows, и у нас уже был печальный опыт портирования Windows Forms. Под Linux мы можем использовать GtkSharp, но идея ставить Mono на Windows при наличии .Net мне не нравится. В итоге приходится писать и поддерживать отдельный интерфейс под каждую ОС.
Что в этой ситуации могла придумать команда .Net(с уклоном под веб)? Решили встраивать Webkit и писать GUI на связке html-js-css.
На сегодняшний день мы 2 года успешно используем такой подход для Windows и год – под Linux и Mac. До мобильной платформы пока руки не дошли.


Зачем?


Идентичный интерфейс под всеми платформами. Возможны лишь незначительные отличия при отрисовке шрифтов, при отображении элементов. Последнее всегда объясняется ошибками в верстке.
Разработка под одну ОС. Эмпирическим путем нами было выявлено, что достаточно вести основную разработку под Windows, а под остальными платформами лишь иногда проверять. Например, перед релизом.
Вся сила веб-разработки. Особенно это актуально, если команда состоит из веб-разработчиков. Можно использовать html5, css3, привычные подходы и библиотеки. Мы, кстати, используем популярный фреймворк для построения веб-приложений, в итоге у нас интерфейс только на js.
Разделение на frontend и backend. Появляется возможность вести отдельно разработку представления и логики приложения, согласовав апи. Например, наш интерфейс — это полноценное веб-приложение, взаимодействующее с «сервером» через ajax-запросы. В десктоп приложении эмулируем обработку этих запросов. Таким образом, можно разрабатывать и отлаживать интерфейс с использованием инструментов разработчика в Chrome, закинув необходимые mock ответы на локальный сервер. Особо уверенные в себе разработчики, которым достаточно доступа к dom и консоли, могут использовать firebug lite в десктоп приложении.
Есть о чем написать на хабр. Подобные эксперименты добавляют азарта при разработке и скрашивают суровые будни программиста.

Как?


Под каждую платформу создаем нативное приложение, GUI которого состоит из одного элемента пользовательского интерфейса – браузера, растянутого во все окно.
Нам нужно научиться отображать html в браузере, найти способ осуществлять вызовы js-C# и С#-js. Различия в вызовах могут показаться странными, но есть простое объяснение – в используемых браузерах реализован и работает разный функционал.

Mac OSX

Выбора что встраивать под маком нету. Поэтому используем MonoMac и стандартный браузер. Но тут есть подвох в лицензиях. Можно свободно распространять приложение без Mono, т.е. пользователь сам должен будет поставить Mono и, следовательно, приложение не может попасть в AppStore. Если же мы хотим встроить Mono в приложение, то придется покупать Xamarin.Mac, который обойдется в 300 или 1000 долларов в зависимости от размера компании за одного программиста.
Под мак получился самый лаконичный код. Единственное не интуитивно понятное место — вызов С# из js.
После инициализации браузера нам надо создать объект, через который js сможет вызывать методы контроллера из C#. Назовем объект interaction:
    webView.WindowScriptObject.SetValueForKey(this, new NSString("interaction"));

Определяем методы и указываем, какие из них могут быть вызваны из js:
    [Export("callFromJs")]
    public void CallFromJs(NSString message)
    {
        CallJs("showMessage", message + " Ответ из C#");
    }

    [Export ("isSelectorExcludedFromWebScript:")]
    public static bool IsSelectorExcludedFromScript(MonoMac.ObjCRuntime.Selector sel)
    {
        if (sel.Name == "callFromJs")
            return false;

        return true; // Запрещаем вызов всех остальных методов
    }

Теперь в js мы можем вызвать метод сallFromJs:
    window.interaction.callFromJs('Вызов из js.');

Полный листинг заявленного функционала с комментариями
    public partial class MainWindowController : MonoMac.AppKit.NSWindowController
    {
        /*Автоматически сгенерированный код*/

        //Интерфейс из xib(nib) построен, инициализорован и все ссылки на UI компоненты установлены
        public override void AwakeFromNib ()
        {
            base.AwakeFromNib ();

            // Создаем объект через который js сможет обращаться к C#. Назовем его interaction
            // window.interaction.callFromJs(param1, param2, param3) - вызываем метод из js.
            webView.WindowScriptObject.SetValueForKey(this, new NSString("interaction"));

            webView.MainFrame.LoadHtmlString (@"
                <html>
                    <head></head>
                    <body id=body>
                        <h1>Интерфейс</h1>
                        <button id=btn>Вызвать C#</button>
                        <p id=msg></p>

                        <script>
                            function buttonClick() {
                                interaction.callFromJs('Вызов из js.');
                            }
                            function showMessage(msg) {
                                document.getElementById('msg').innerHTML = msg;
                            }

                            document.getElementById('btn').onclick = buttonClick;
                        </script>
                    </body>
                </html>", null);

        }

        // Из соображений безопасности указываем, какие методы могут быть вызваны из js
        [Export ("isSelectorExcludedFromWebScript:")]
        public static bool IsSelectorExcludedFromWebScript(MonoMac.ObjCRuntime.Selector aSelector)
        {
            if (aSelector.Name == "callFromJs")
                return false;

            return true; // Запрещаем вызов всех остальных методов
        }

        [Export("callFromJs")]
        public void CallFromJs(NSString message)
        {
            CallJs("showMessage", new NSObject[] { new NSString(message + " Ответ из C#") });
        }

        public void CallJs(string function, NSObject[] arguments)
        {
            this.InvokeOnMainThread(() =>
            {
                webView.WindowScriptObject.CallWebScriptMethod(function, arguments);
            });
        }
    }


Рабочий пример на github.
Этого видео мне очень не хватало, когда я разбирался: «Как добавить ссылку на WebView в код контроллера».


Ubuntu

Под Mono используем пакет webkit-sharp.
Плавно увеличивается количество не интуитивно понятного кода.
Для вызова C# из js можно перехватывать переход по ссылке.
    browser.NavigationRequested += (sender, args) =>
    {
        var url = new Uri(args.Request.Uri);
        if (url.Scheme != "mp")
        {
            //mp - myprotocol.
            //Обрабатываем вызовы только нашего специального протокола.
            //Переходы по обычным ссылкам работают как и прежде
            return;
        }
            
        var parameters = System.Web.HttpUtility.ParseQueryString(url.Query);
        handlers[url.Host.ToLower()](parameters);

        //Отменяем переход по ссылке
        browser.StopLoading();
    };

Вызов из js будет выглядеть так:
    window.location.href = 'mp://callFromJs?msg=Сообщение из js.';

Еще один способ завязывается на событие TitleChanged.
В js устанавливаем title у документа:
    document.title = JSON.stringify({
        method: 'callFromJs',
        arguments: { msg: 'Сообщение из js'}
    });

В С# срабатывает событие TitleChanged, мы десериализуем title и аналогично предыдущему подходу вызываем обработчик.

В рассматриваемой обертке WebKit можно из С# исполнять любой js код, что позволяет нам реализовать вызов js из C#:
    public void CallJs(string function, object[] args)
    {
        //Формируем javascript
        var js = string.Format(@"
            {0}.apply(window, {1});
        ", function, new JavaScriptSerializer().Serialize(args));

        Gtk.Application.Invoke(delegate {
            browser.ExecuteScript(js);
        });
    }

Полный листинг заявленного функционала с комментариями
    public partial class MainWindow: Gtk.Window
    {
        private Dictionary<string, Action<NameValueCollection>> handlers;
        private WebView browser;

        public MainWindow (): base (Gtk.WindowType.Toplevel)
        {
            Build ();

            CreateBrowser ();

            this.ShowAll ();
        }
        
        protected void OnDeleteEvent (object sender, DeleteEventArgs a)
        {
            Application.Quit ();
            a.RetVal = true;
        }

        private void CreateBrowser ()
        {
            //Создаем массив обработчиков доступных для вызова из js
            handlers = new Dictionary<string, Action<NameValueCollection>>
            {
                { "callfromjs", nv => CallJs("showMessage", new object[] { nv["msg"] + " Ответ из С#" }) }
            };

            browser = new WebView ();

            browser.NavigationRequested += (sender, args) =>
            {
                var url = new Uri(args.Request.Uri);
                if (url.Scheme != "mp")
                {
                    //mp - myprotocol.
                    //Обрабатываем вызовы только нашего специального протокола.
                    //Переходы по обычным ссылкам работают как и прежде
                    return;
                }
                
                var parameters = System.Web.HttpUtility.ParseQueryString(url.Query);

                handlers[url.Host.ToLower()](parameters);

                //Отменяем переход по ссылке
                browser.StopLoading();
            };

            browser.LoadHtmlString (@"
                    <html>
                        <head></head>
                        <body id=body>
                            <h1>Интерфейс</h1>
                            <button id=btn>Вызвать C#</button>
                            <p id=msg></p>

                            <script>
                                function buttonClick() {
                                    window.location.href = 'mp://callFromJs?msg=Сообщение из js.';
                                }
                                function showMessage(msg) {
                                    document.getElementById('msg').innerHTML = msg;
                                }

                                document.getElementById('btn').onclick = buttonClick;
                            </script>
                        </body>
                    </html>
                ", null);

            this.Add (browser);
        }

        public void CallJs(string function, object[] args)
        {
            var js = string.Format(@"
                {0}.apply(window, {1});
            ", function, new JavaScriptSerializer().Serialize(args));

            Gtk.Application.Invoke(delegate {
                browser.ExecuteScript(js);
            });
        }
    }


Рабочий пример на github.

Windows

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

Особенности

У такого интересного способа представления интерфейса есть свои особенности, о которых стоит знать, если вы решите повторить наш путь.
Дополнительный расход времени при строительстве. Подготовка для встраивания интерфейса как ресурса в приложение занимает некоторое время: Saas, склеивание файликов, минификация. Но зато при разработке интерфейса в браузере нет необходимости каждый раз перестраивать ни интерфейс, ни само приложение.
Увеличение расхода оперативной памяти. Это единственный серьезный минус данного подхода. Браузер в нашем случае потребляет мегабайт 50 оперативной памяти. С одной стороны это немного, но если целевая аудитория предполагает старую технику, то придется принимать во внимание эту особенность. Хотя будет ли аналогичный интерфейс, реализованный на другой технологии, потреблять меньше памяти – непонятно. В любом случае, у нас расход оперативной памяти браузером – черный ящик. Других системных проблем или проседаний производительности нами замечено не было.
Автор: @JacobL
LLC Tik-Tok Coach
рейтинг 20,73
Компания прекратила активность на сайте
Похожие публикации

Комментарии (41)

  • 0
    Благодаря Mono мы можем писать кроссплатформенные приложения, но вопрос с GUI остается открытым.
    wxWidgets не пробовали?
    • 0
      Не пробовали. И я первый раз вижу этот враппер.
      Когда мы начинали разработку, мы планировали в скором времени выпустить и мобильную версию. И поэтому интерфейс на html+js+css нам кажется более подходящим.
  • +4
    Для кроссплатформенной разработки десктопных приложений на C# можно использовать следующие GUI фреймворки:
    XWT github.com/mono/xwt
    Eto github.com/picoe/Eto/
    • 0
      Я видел эти проекты, но у нас очень высокие требования к гибкости интерфейса, к возможности добавления своих компонентов. Есть опасение, что придется писать в итоге все равно под каждую платформу по отдельности, как и предлагается в Eto:
      «For advanced scenarios, you can take advantage of each platform's capabilities by wrapping your common UI in a larger application, or even create your own high-level controls with a custom implementations per platform.»
  • +4
    А почему вебкит, почему не CEF? С CEFGlue можно использвать одну единственную dll-ку биндингов на всех платформах, например.
    • +1
      Когда писали Windows версию, CEF не попался на глаза, хотя судя по истории он появился на пару месяцев раньше. Когда делали обертку под Mac, мне он показался по каким-то причинам не рабочим под этой платформой. А сейчас у нас все работает с имеющейся реализацией и предпосылок для переписывания нету, да и других задач хватает.

      Но в целом, мне идея с CEF очень нравится.
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      Мы пишем обертку над браузерами чтобы унифицировать взаимодействие. Дальше логика приложения работает с оберткой.
      Получается что под разными платформами отличается только обертка(строк по 100 кода), а вся логика и весь интерфейс абсолютно одинаковый.

      Видео с GUI
      • 0
        Можно поступить проще. Использовать CefSharp в качестве браузера github.com/cefsharp/CefSharp — это chromium.
  • 0
    пользователь сам должен будет поставить Mono и, следовательно, приложение не может попасть в AppStore
    Вообще говоря при наличии прямых рук, навыков использования otool и скрипта, прописывающего DYLD_LIBRARY_PATH, можно сделать упаковку в бандл самостоятельно.
    • +1
      Я с этим согласен, но разве мы в этом случае не нарушаем лицензионное соглашение?
      • +3
        MIT и LGPL-то?
        • 0
          Извиняюсь, что долго не отвечал. Вспоминал(гуглил).

          «GNU LGPL позволяет линковать с данной библиотекой или программой программы под любой лицензией, несовместимой с GNU GPL, при условии, что такая программа не является производной от объекта, распространяемого под (L)GPL, кроме как путём линкования.»

          Встроить в бандл — это явно ведь не линкование.

          «The Mono runtime license is a commercial license that allows developers to redistribute their Mono-based applications without being bound by the terms of the GNU LGPL v2. This allows you to publish both to the Apple App Store as well as distributing applications that embed the Mono runtime without having to provide source code or object files for end users to relink.» Взято здесь

          И мы хотим распространять наш продукт под коммерческой лицензией
          • +1
            Встроена в бандл — это значит вы просто поставляете ее вместе с программой, но вы по-прежнему динамически линкуетесь с ней, так что нарушения лицензии не будет.
  • +1
    Интересный подход. Правда я, если есть потребность в написании кросс-платформенного приложения, использую Qt. Язык разметки интерфейса сильно HTML/CSS напоминает и WEB разработчикам будет довольно просто на него перейти. Единственное, на iOS это перенести не получится. А вот с Андроид ситуация улучшается
  • 0
    То есть вы в своем основном продукте принесли в жертву его самую важную часть (user experience) в угоду экономии на разработке? :(
    • 0
      Совсем не обязательно. Иначе, получается, что во всех веб-сайтах UX принесен в жертву. Причём просто принесен, не ради чего-то.
      • 0
        Веб-приложения — это отдельная история.
        Вы не обращали внимания, что с появлением магазинов приложений на десктопах, появляются приложения популярных веб-сервисов (для одного ВК приложений понаделана уйма)? И надо сказать, что пользоваться ими зачастую удобнее, чем веб-версиями (встраивание в систему, отзывчивость, оффлайн, ярлык для запуска приложения в удобном месте и т.д.).

        В конечном счете сейчас идет тенденция к стиранию границы между сайтом и традиционными десктоп-приложениями — теперь есть сервер/API и есть клиенты, одним из которых является сам сайт.
        А разработать клиент-приложение для десктопа на нативных технологиях зачастую даже проще, чем веб-версию на html/css/js.
        • 0
          А собственно, почему разработка нативного приложения должна быть сложнее?
          Это по-определению проще, чем разработать веб-приложение, работающее в зоопарке браузеров, на разных разрешениях, в разных ОС, с неустоявшимся набором практик и инструментов.
          • +1
            Вы столкнётесь с зоопарком версий Windows, с лгущим о размерах окна Windows.Forms/WPF, нехорошими людьми, вещающими глобальные перехваты WinAPI, параноидальными антивирусами и криворукостью админов организаций. На линуксе ещё и зоопарк дистрибутивов. В общем, много весёлого и занимательного, о котором веб-разработчики не подозревают.
    • +1
      А я не согласен, что у нас user experience пострадал.
      Во-первых, пользователь, меняя платформу, видит абсолютно такую же программу. Ему не нужно искать привычный функционал. Это фича!
      Во-вторых, разработка трех интерфейсов параллельно сказалась бы на количестве багов.
      В-третьи, html-js-css очень хорошо кастомизируется под любую прихоть пользователя.
      • +3
        Во-первых, пользователь, меняя платформу, видит абсолютно такую же программу. Ему не нужно искать привычный функционал. Это фича!

        Все-таки рядовой пользователь чаще переключается между приложениями, чем меняет платформу. Сложно назвать это фичей.
        • +4
          В наше время это уже не факт. На работе Windows, дома MacOS или Ubuntu, в дороге Android или iOS — совсем не редкость.
        • +1
          Согласен с VolCh. Мне очень удобно, что все программы, с которыми я работаю, имеют одинаковый интерфейс на всех платформах. Большинство программ из сферы разработки и повышения продуктивности, а так же браузеры, почтовые клиенты и т.д. только выигрывают от кроссплатформенного интерфейса.
      • 0
        > Во-вторых, разработка трех интерфейсов параллельно сказалась бы на количестве багов.

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

        > В-третьи, html-js-css очень хорошо кастомизируется под любую прихоть пользователя.
        Веб-приложения на мобильных устройствах уступают в производительности нативным, и под эту прихоть пользователя («хочу, чтобы летало… фью… фью..») Вам HTML+CSS+JS, возможно, не удастся кастомизировать.
    • +1
      Идентичный интерфейс под всеми платформами можно рассматривать как «баг», а можно как фичу. Всё зависит от ЦА.
      • 0
        Ну например менюшки на яваскрипте вместо привычных пользователю системных контекстных меню сложно назвать фичей.
        Да там даже заголовок окна несистемный — то есть абсолютно все контролы приложения ведут себя непредсказуемым образом.

        Я не спорю, кроссплатформенная разработка — это здорово, но я для себя пока плохо представляю, как совместить единый код и различные интерфейсы (а в разных ОС не только внешнее оформление, но и некоторые базовые парадигмы различны).

        К тому же, это приложение нельзя опубликовать в Mac App Store — а это потеря еще одного канала продвижения приложения.
        • 0
          >>Да там даже заголовок окна несистемный
          Он только под виндой несистемный. Специально выпиливали потому что не понравилось как выглядит приложение в стандартном окне. В общем, это тоже фича :( Под Ubuntu и Mac системные окна.

          >>К тому же, это приложение нельзя опубликовать в Mac App Store
          Почему нельзя? Можно — документация
        • 0
          Каких системных контекстных? По оформлению или по содержанию? Насколько я помню разработку под десктоп, то содержанием управляет исключительно приложение.

          А вообще решение писать кроссплатформенный клиент с единым интерфейсом может быть обусловлено множеством причин, от маркетинговых (ЦА нужна именно предсказуемость приложения независимо от платформы) до технико-экономических (в команде нет людей, способных за разумное время создать и поддерживать нативный код под десяток платформ) и идеологических (люди есть, но из принципа не хотят).
  • 0
    Спасибо за статью. Можно несколько вопросов?
    1. А как у вас с потреблением ресурсов?
    2. Сколько вышел размер итогового приложения? У вас с виду довольно компактное приложение. Не напрягает ли клиентов то, что вы толстый вебкит тащите?
    3. Насколько отзывчив интерфейс? Видео просмотрел, но не очень чувствуется время отклика, когда сам не пробовал.
    4. Как решили вопрос с кастомизацией диалогов и модальных окон?

    Сейчас как раз делаю нечто подобное. Только использую Xilium.CEFglue Вы пробовали какие-то другие инструменты? Судя по видео, с вашей задачей неплохо бы справился Adobe Air. И довесок в виде среды исполнения был бы копеечный.
    • 0
      Небольшое уточнение к первому вопросу. Я видел в посте про потребление оперативной памяти. Хотелось узнать как это сказалось на ваших клиентах? Не было ли обращений в службу поддержки по этому поводу?
      • +1
        1. На потребление ресурсов жалоб не было. Может быть еще будут :))
        2. Под Windows приложение 35 мегабайт. Плюс надо поставить 3 распространяемых пакета сумарно 15 мб. Под linux deb пакет 1.5 мегабайта, но он тянет зависимости. Не возьмусь оценить сколко это, а Ubuntu под рукой нету. Под мак приложение 50 мб вместе со встроенным Mono runtime. A installer для мака 10 мегабайт.
        Я не думаю, что это много.
        3. Отзывчивость интерфейса зависит от платформы. Нас пока устраивает, но иногда ощущаются тормоза. Тут стоит принять во внимание, что еще и интерфейс у нас «тяжелый», писали на Sencha Touch.
        4. Никак не кастомизируем системные модальные окна. Практически их не используем. И в интерфейсе используем модальные окна от Sencha Touch.

        Под виндой еще пробовали Awesomium. Под линукс и мак у нас сходу завелись указанные компоненты. Их и используем.
        Про Adobe Air не думали.

        P.S. Если хотите оценить отклик, Вы можете скачать клиента на нашем сайте. Но придется зарегистрироваться. Если не хотите лишних писем, то можно использовать 10 minute mail. И мы не выкладываем клиента под мак в общий доступ, потому что хотим распространять через app store. Поэтому если хотите, то я могу отправить.
        • 0
          Спасибо за ответы. Посмотрю приложение. А XULRunner + GeckoFX не смотрели? Там рантайм немного меньше будет по размеру.
          • 0
            Нет, не смотрели. Sencha Touch под Gecko не работает
  • +1
    Не совсем понятно для чего использовать разные решения под каждую ОС. Ведь можно просто взять например враппер для гейко и использовать его под каждой ОС, это конечно не решит вопрос с лицензиями под осикс но все же уменьшит танцы с бубном под различные ос.
    • 0
      Уточните, пожалуйста, что такое гейко.
      • 0
        Простите за неверную транскрипцию, но вы наверное уже догадались что я про GeckoFX, хотя есть подобные решения, обертки для webkit.
  • 0
    А QT нет под C#?
    • 0
      Есть. Но там тоже уходят от виджетам к QML. Мы вот новую версию кебрумовского клиента писали в смешанном режиме — системная логика — C++, весь гуй — HTML + JS через QtWebKit, получилось намного быстрее и красивее чем на виджетах.
  • 0
    На все вопросы, возникшие у меня во время прочтения статьи, ответы в комментариях я получил. Интересует лишь одно: Вас не смущает, что webkit.net последний релиз был года этак 3 назад?
    • 0
      Проект, видимо, перестал развиваться. Но имеющаяся версия справляется с отображением нашего GUI. И у нас реально ситуация «написал и забыл». Два года уже пашет webkit.net и практически о себе не напоминает, критичных багов в нем не замечено. В общем, проверенное временем решение.

      Как появятся серьезные предпосылки, переедем на что-нибудь другое. Скорее всего на CEF.

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

Самое читаемое Разработка