Абстрактный UI. Новый фреймворк для C#, который назвали XWT

    Если посмотреть на существующие современные фреймворки для реализации пользовательских интерфейсов (WPF, GTK, Qt, wxWidgets, да даже модные веб-решения), то легко заметить, что они похожи друг на друга как близнецы. Любой фреймворк содержит кнопки, поля, чекбоксы, переключатели, характеризуемые идентичной с точки зрения пользователя логикой. Отличия заключаются только в низкоуровневой реализации.

    Когда где-то в мире программирования появляется что-то слишком похожее, то это стремятся обернуть в слой абстракции. И вот я случайно наткнулся на подобное решение, созданное парнями из Xamarin. Теми самыми, которые продают C# фреймворк для iOS и Android. Решение это назвали Xwt — судя по всему, это расшифровывается как Xamarin Window Toolkit.



    Абстрактно




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

    Сначала про интерфейс: поскольку задачей Xwt было портирование MonoDevelop с Gtk на Cocoa, интерфейс Xwt крайне близок к интерфейсу Gtk#. Контролы размещаются в автоматически масштабируемых строчных, столбцовых и табличных layout'ах в соответствии с унаследованными от GTK правилами fill и expand. Также от Gtk сохранилось устройство TreeView/TreeStore, хоть и несколько приблизилось к стандартам .NET.

    В существующей версии Xwt есть реализации для WPF, Gtk и Cocoa. При этом никто не запрещает использовать Gtk на Windows или Mac OS X.

    Забрать фреймворк можно в исходниках отсюда. Интерфейс отдельно собирается в Xwt.dll, а сборки — в Xwt.Gtk.dll, Xwt.Mac.dll, Xwt.WPF.dll. При этом варианты реализации и ссылки на них жестко прописаны в головном файле, так что добавить свои реализации можно только форкнув весь проект.

    Попробуем сделать маленькое приложение на Xwt:
    class Program
        {
            [STAThread]
            static void Main(string[] args)
            {
                Xwt.Application.Initialize(Xwt.ToolkitType.Wpf);
                Xwt.Window MainWindow = new Xwt.Window()
                {
                    Title = "Xwt Test"
                };
                MainWindow.CloseRequested += (o, e) =>
                {
                    Xwt.Application.Exit();
                };
                Xwt.Menu MainMenu = new Xwt.Menu();
                Xwt.RichTextView TextView = new Xwt.RichTextView();
                Xwt.MenuItem FileOpenMenuItem = new Xwt.MenuItem("Открыть");
                Xwt.Menu FileMenu = new Xwt.Menu();
                FileOpenMenuItem.Clicked += (o,e) => {
                    Xwt.OpenFileDialog Dialog = new Xwt.OpenFileDialog("Открыть файл");
                    if (Dialog.Run(MainWindow)) {
                        TextView.LoadFile(Dialog.FileName, Xwt.Formats.TextFormat.Markdown);
                    }
                };
                Xwt.MenuItem FileMenuItem = new Xwt.MenuItem("Файл") { SubMenu = FileMenu };
                FileMenu.Items.Add(FileOpenMenuItem);
                MainMenu.Items.Add(FileMenuItem);
                MainWindow.MainMenu = MainMenu;
                MainWindow.Content = TextView;
                MainWindow.Show();
                Xwt.Application.Run();
            }
        }
    


    Обратите внимание, что соответствующие интерфейсы сделаны не только для контролов, но и для стандартных диалоговых окон. В качестве бонуса в контрол RichTextView встроен парсер Markdown'а :)

    Вот что мы увидим, выполнив это приложение:


    Заменяем одну строку на Xwt.Application.Initialize(Xwt.ToolkitType.Gtk); и получаем другой результат.


    Еще больше абстракции


    Авторы пошли еще дальше и включили абстрактный интерфейс рисования, близкий по внешнему интерфейсу к Gdk. В рамках этого интерфейса присутствует, например, Xwt.Drawing.Color, отличный и от System.Drawing.Color, и от Gdk.Color.

    Вот простой код, рисующий кружок. Он успешно выполняется на всех платформах.
    class DrawCircle : Xwt.Canvas
        {
            protected override void OnDraw(Xwt.Drawing.Context ctx, Xwt.Rectangle dirtyRect)
            {
                ctx.SetColor(Xwt.Drawing.Colors.Black);
                ctx.SetLineWidth(1);
                ctx.Arc(50, 50, 30, 0, 350);
                ctx.Stroke();
            }
        }
        
        class Program
        {
            [STAThread]
            static void Main(string[] args)
            {
                Xwt.Application.Initialize(Xwt.ToolkitType.Wpf);
                Xwt.Window MainWindow = new Xwt.Window()
                {
                    Title = "Xwt Test"
                };
                MainWindow.CloseRequested += (o, e) =>
                {
                    Xwt.Application.Exit();
                };
                DrawCircle Canvas = new DrawCircle();
                MainWindow.Content = Canvas;
                MainWindow.Show();
                Xwt.Application.Run();
            }
        }
    


    С одной стороны, кроссплатформенный интерфейс рисования — это вещь, безусловно, необходимая. С другой стороны, существующий код нельзя перенести, ни если он написан для System.Drawing, ни для Cairo.

    Большие претензии


    Кроме абстрактного движка рисования, авторы фреймворка замахнулись на создание абстракции для анимаций. На текущем этапе он представляет что-то похожее на $.animate() ранних версий: простая обертка над таймером. Мне сложно было представить анимированный Gtk#, поэтому я просто попробовал сделать простой shake, как в модных окнах логина.

     class Program
        {
            [STAThread]
            static void Main(string[] args)
            {
                Xwt.Application.Initialize(Xwt.ToolkitType.Gtk);
                Xwt.Window W = new Xwt.Window() { };
                Xwt.Button B = new Xwt.Button("Animate");
                W.Content = B;
                W.Show();
                B.Clicked += (o, e) =>
                {
    #region Костыль
    //Авторы чего-то напутали в системах координат, в результате чего безобидный код W.X = W.X сдвигает окно на какое-то расстояние, разное для каждого окна. Маленький костыль исправляет ситуацию. Мелькание окна иногда проскакивает, иногда нет.
                    double CurX = W.X, CurY = W.Y;
                    W.X = CurX;
                    W.Y = CurY;
                    double DiffX  = W.X - CurX, DiffY = W.Y - CurY;
                    W.X = CurX - DiffX;
                    W.Y = CurY - DiffY;
    #endregion
                    W.Animate("", (X) =>
                    {
                        W.Location = new Point((CurX - DiffX) + 8 * Math.Sin(20 * X), (CurY - DiffY));
                    }, length: 750, repeat: () => false);
                };
                Xwt.Application.Run();
            }
        }
    


    Вышеприведенный пример успешно сработал и с ToolkitType.Gtk, и с ToolkitType.Wpf.

    Вместо заключения


    Сейчас это весьма сырой, open-source проект, распространяемый по лицензии MIT, у него вообще нет документации, никакой, вообще. Тем не менее, на нем написана целая IDE (MonoDevelop 4). В рамках проекта все еще нет графического дизайнера, и пока непонятно, по какому пути пойдут разработчики с декларативным языком — форк Glade или адаптация XAML. Уже сейчас фреймворк можно использовать для academic проектов в связке с Mono, а с появлением хотя бы самого простого декларативного языка это будет еще и удобно. Забавно, на мой взгляд, было бы реализовать бэкенд на jQuery с разворачиванием локального веб-сервера: это позволило бы выполнять такие academic решения вообще без какой-либо платформы UI, например, на виртуальной машине в облаке. При этом сохранялся бы принцип для слабаков «пиши один раз, запускай везде». А еще прямо в облаке можно было бы запустить сам MonoDevelop :) Есть маленькая вероятность, что Xamarin в процессе дружбы с Microsoft со временем представит Xwt как основное решение Microsoft для кросс-платформенных UI, и тогда этот сырой бесплатный проект будет везде.

    Некоторые эксперты полагают, что пытаться заинтерфейсить существующие тулкиты UI на разных платформах — задача по определению не имеющая адекватного решения, потому что различные платформы обладают различным user experience. Xwt, впрочем, вполне работоспособен. Его минус в том, что он охватывает жесткий набор контролов и поведений, и тем самым требует от бэкенда реализацию всех этих интерфейсов. Значительно лучше было бы, если бы каждый контрол представлял собой подобие контракта; приложение указывало бы набор таких контрактов и выполнялось бы только на тех фреймворках, где эти контракты доступны.

    А еще хотелось бы попросить владельцев макинтошей попробовать потестировать работу фреймворка в различных режимах.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 42

      +1
      Авторы пошли еще дальше и включили абстрактный интерфейс рисования, близкий по внешнему интерфейсу к Gdk. В рамках этого интерфейса присутствует, например, Xwt.Drawing.Color, отличный и от System.Drawing.Color, и от Gdk.Color.
      Вот чем им System.Drawing не угодил, работает же везде (Cairo в качестве бакэнда у libgdiplus). Так и приходится колдовать с созданием Bitmap-ов, указывающих на один физический участок памяти.
        0
        Для меня это тоже было разочарованием, но причина, как я полагаю, совершенно очевидная — в MonoDevelop уже были куски кода на Mono.Cairo.
          +2
          На момент 4х лет назад System.Drawing был написан осьминогами. Цвета перепутаны, точку без дополнительного гемора не нарисовать, аналога bitblt не было вообще, индексированные палитры не поддерживались. Сейчас не знаю.
            0
            Цвета перепутаны
            Это как?
            аналога bitblt не было вообще
            Вам надо картинки рисовать или дёргать куски с одного Graphics на другой? Первое делается через graphics.DrawImage, второе в общем случае невозможно, потому что Graphics может быть не только поверх битмапа. Если хотите двойную буферизацию, создавайте Graphics поверх Bitmap и уже этот битмап рисуйте через DrawImage.

            С портабельностью на никсах я заметил ровно две проблемы
            1) тупит изменение размера картинок, на выходе очень некрасиво
            2) нельзя зеркалировать картинки, задавая целевому прямоугольнику отрицательные размеры
              0
              > Это как?
              А вот так ) Значения именованных цветов не совпадали с реальностью — ис того, что помню darkgray светлее чем gray.

              > Вам надо картинки рисовать или дёргать куски с одного Graphics на другой?
              Таки надо было дёргать куски Graphics. Делал велоконтрол для отображения древовидной структуры. Нужно было при открытии/закрытии структуры сдвигать часть изображения. Пришлось дёргать bitblt (вот в эту трубу, похоже, вся переносимость на Mono и улетает). Двойная буферизация — средствами WinForms.

              Что характерно, с этой задачей bitblt превосходно справлялся: достаточно было просто передать ему хэнд контекста из Graphics.

              А уж рисовать точки прямоугольниками — это вообще нечто.
                0
                Таки надо было дёргать куски Graphics. Делал велоконтрол для отображения древовидной структуры. Нужно было при открытии/закрытии структуры сдвигать часть изображения. Пришлось дёргать bitblt (вот в эту трубу, похоже, вся переносимость на Mono и улетает)
                Вы его откуда брали, из текущего состояния экрана? Это не очень хорошая идея как бы. Если не брать с экрана, то никаких проблем с реализацией двойной буферизации со своим Graphics поверх битмапа я не вижу. В целом же решение типа «сдвигать часть изображения» выглядит как костыль.
                  0
                  Да не было там битмаба, там был Graphics юзерконтрола и включённая буферизация у него же ) То есть, я тупо рисовал на контроле в событии отрисовки. bitblt делал для эко контекста из самого в себя, нехватающее дорисовывал. В принципе, это даже работало и нормально выглядело.
                    0
                    В принципе, это костыль. Хотите использовать уже отрисованные данные — делайте Graphics.FromBitmap, рисуйте на нём и используйте потом исходный битмап в качестве источника. Сферический же Graphics в вакууме может вообще не использовать внутри битмап — он может представлять принтер, например.
                      0
                      Тут возникает встречный вопрос: зачем нужна эта высокоуровневая абстракция, если всё равно приходится спускаться вниз на конкретную реализацию? А вдруг я захочу нарисовать тоже самое именно на контексте принтера? Имхо, костыль тут как раз FromBitmap. Впрочем, и вызов bitblt тут костыль… И за это System.Drawing мне и не нравится — без костылей не обойтись.
                        0
                        высокоуровневая абстракция
                        Потому что это абстракция не над картинкой, это абстракция над «штукой, на которой можно рисовать», нигде не сказано, что её при этом можно использовать в качестве источника. Хотите нечто, на чём можно рисовать и использовать в качестве источника? Создайте источник и прикрепите к нему контекст рисования, это логично и правильно с архитектурной точки зрения.
                        А вдруг я захочу нарисовать тоже самое именно на контексте принтера?
                        То же самое в контексте принтера вы не можете нарисовать без своей буферизации, потому что ваш код хочет читать данные с контекста рисования, а с принтера нельзя читать.
                        Вы же не жалуетесь на то, что из сокета нельзя прочитать ранее отправленные данные, верно?
                          0
                          Graphics в случае принтера рисуется в векторе. Там нельзя просто так взять, и использовать растровый битмап. И на кой хрен там буферизация-то? Каждая страница отрисовывается 1 раз в специальном классе…
                          Если я верно помню, с контекста Graphics можно читать всегда. Вызываете берёте getHdc, получаете хэндл контекста и используете WinAPI.
                            0
                            Если я верно помню, с контекста Graphics можно читать всегда.
                            BitBlt обычно не в может прочитать с hDC принтера. Более того, она в ряде случаев туда и писать оказывается не в состоянии.
                              0
                              Точно не скажу, конкретно с контекстом принтера bitblt я не использовал. В любом случае, непонятно, что им мешало сделать так, как это реализовано в Stream: разные виды потоков могут не поддерживать запись или произвольное чтение, но api есть для всего, + набор флагов, позволяющий проверить возможности конкретного потока не проверяя его класс.
                                0
                                Специально для таких как вы кто-то сделал костыль, радуйтесь. github.com/mono/libgdiplus/commit/969f5c03e9b297ee3ba1b5c42bf5bb920286ab64
                                  0
                                  Во-первых, вопрос на засыпку: а оно работает под моно в *nix? Судя по коду — нет. А раз так — импорт функции с pinvoke.net/ скопипастить не проблема.
                                  Во-вторых — вы опоздали на… *считает* 4 года.

                                  — Хм. Чукча не читатель. Но каким образом оно совместимо с этим самым импортом виндовой API-функции, написанной под .NET?
                                    0
                                    1) На windows моно использует системную gdiplus.dll
                                    2) На *nix hDC ненастоящий и является как раз контекстом Cairo. BitBlt через dllmap направляется на libgdiplus.so, где и происходит эта обработка. Этот персонаж сей патч очень долго продвигал в апстрим (костыль для какой-то проприетарщины).
                                      0
                                      Я боюсь, что это костыль для очень большого числа софта. stackoverflow просто наполнен советами использовать bitblt для копирования части контекста.
              0
              Полностью согласен.
              В GDIPlus нельзя элементарно нарисовать точку, нельзя стереть участок картинки, совсем,
              как можно было создать такую ограниченную графическую библиотеку?
                +1
                В GDIPlus нельзя элементарно нарисовать точку
                Строго говоря отсутствие PSet призвано сократить количество говнокода, таким образом работающего с битмапом попиксельно. Если очень хочется — сделайте себе extension-метод, который заполняет прямоугольник единичного размера.
                нельзя стереть участок картинки, совсем

                Документацию вы не читали, совсем.
                g.CompositingMode = CompositingMode.SourceCopy;    
                g.FillRectangle(Brushes.Transparent,x,y,width,height);
                
            +1
            Он реализует какие-нибудь серьёзные парадигмы в плане реализации интерфейса (типа MVVM, MVC, шаблоны из WPF, HTML+CSS, сигналы+слоты Qt, универсальные виджеты KDE Plasma и пр.)? Есть какая-нибудь крутая фишка? Или это просто примитивный многоплатформенный WinForms?
              +4
              сигналы+слоты Qt
              Зачем они вам в языке, поддерживающем события?
              типа MVVM, MVC
              Model-View-Presenter и Model-View-Presenter-ViewModel вполне успешно применимы на тех же Windows Forms, DataBinginds у контролов вполне хватает для привязки к свойствам модельки, поддерживающей INotifyPropertyChanged. Зачем вам для этого дополнительная поддержка со стороны тулкита, мне не особо ясно.
                +1
                Крутая фишка — это native look&feel на Windows, Linux и Mac OS, с поддержкой родных диалоговых окон, системных кастомизаций и т.д. Выглядит и пахнет он в точности как Gtk#, даже немного лучше — сделаны нормальные обертки для сложных виджетов вроде ListView/TreeView. MVVM/MVC/шаблоны — это то, что можно автоматизировать, используя существующий код Xwt.
                  0
                  Я не увидел — bindings есть/будут/нет?
                    0
                    Нет, сейчас их нету. Биндинги — это инфраструктура WPF, которой в Mono вообще нет, или есть в зачаточном состоянии. Эта инфраструктура могла бы появится в рамках Moonlight, но его закрыли. Теперь либо Moonlight переработают в Xwt, либо сделают свои велосипеды. В любом случае рано или поздно они появятся и в Gtk.
                      +1
                      Их не стали копировать, потому что их код просто ужасен, хуже даже чем винапи. Так что правильно сделали.
                      0
                      Заюзайте ReactiveUI )
                  0
                  MonoDevelop 4 — это тот, который Xamarin Studio?
                    +1
                    да, он самый
                    0
                    Только что закончил проект на .NET под Win / WPF + Mac / MonoMac-Cocoa. Эта штука спасла бы меня, а так пришлось писать отдельные фронтэнды :(
                      +1
                      Думаю, она еще слишком сырая, чтобы на ней делать проекты уже сейчас.
                        0
                        Если Xamarin действительно на ней написан, то должно быть уже вполне юзабельно.
                          +1
                          Насколько мне известно, MonoDevelop(Xamarin Studio) пока не завершили полный переход на XWT. На данный момент меню(приложения и контекстное), Android дизайнер и iOS дизайнер(в альфе), а так же файловые диалоги уже реализованы через XWT. Часть остается на gtk с тюнингом от Xamarin.
                      +4
                      Фиговина любопытная, и определённо есть, где её применить. Однако в рамках этой штуки вряд ли удастся реализиовать реально сложный интерфейс, потому что она будет наибольшим общим делителем. Вообще, в .NET так делать не принято: исторически сложился подход, что для каждой среды свой родной слой UI. Кода больше, разумеется, но результат того стоит.

                      Что получается в противном случае — прекрасно видно на примере Java: десктопные приложения работают на любой оси, но выглядят одинаково чужеродно абсолютно везде. В результате для десктопной разработки джава популярной не стала. (Не в курсе, что там с JavaFX происходит, и насколько он выглядит родным на всех поддерживаемых осях. Если кто знает, просвятите.)
                        +2
                        Пример не очень, в отличие от Java, здесь вроде как везде родные компоненты ОС в конечном итоге
                          +1
                          Мне с трудом верится, что удастся сделать полноценный UI фреймворк, полностью оставаясь в рамках наибольшего общего делителя. Это значит, что при добавлении сложных фич будет имитация поведения одного фреймворка костылями в другом. Ну или XWT будет настолько примитивен, что дальше чекбоксов и кнопок он недалеко уйдёт, что тоже не очень вдохновляющий вариант развития событий.
                            0
                            Последний вариант, на мой взгляд, наиболее благополучен. Многим разработчикам не нужны особые фичи UI, а кроссплатформенность нужна. Кроме того существование своеобразного стандарта на контролы подстегнет разработчиков к созданию аналогичных контролов на разных платформах, а там и сложные контролы могут появится.
                              0
                              По идее большая часть контролов во всех оконных тулкитах представляет из себя именно базовый набор, поверх которых идут различного рода layout-ы для удобства их расположения… Остальные вещи по сути уже не входят в базовый набор виджетов ОС, а реализуются на имеющихся средствах тулкита, что Xwt тоже позволяет сделать. В принципе, если какой-то виджет для ОС является уникальным, ничто не мешает сделать реализацию средствами Xwt, которая будет использоваться в других ОС.
                              0
                              Вообще-то в Java есть и то и другое. В начале был создан кросплатформный фреймворк AWT (Abstract Window Toolkit), построенный на родных компонентах системы, он был быстр, выглядел как родной, но был сложен и из-за кроплатформности содержал только те компоненты, которые были во всех ОС. А это значит он был довольно ограничен и неповоротлив. Тогда решили удариться в другу крайность и создали SWING. Он позволял делать безумную кастомизацию, но по всей видимости этим больше злоупотребляли, чем пользовались во благо. SWING может выглядеть как «родной», под виндой, но только если этого захочет разработчик. Под линуксом и маком с этим есть проблемы, правда не значительные.

                              Уровень кастомизации SWING приложений просто не имеет границ, однако, как было замечено, не популярность java на десктопах практически закопало развитие этого фреймворка. Возможно в этом есть резон так как полная прорисовка компонент занимало много ресурсов, и не всегда это было оправданно. По началу были реальные проблемы с производительность конкретно у этого фреймворка. Сейчас, когда прорисовка компонент происходит средствами графических ускорителей и нет дефицита памяти это уже не играет роли. Но уже было принято решение делать JavaFX, а SWING оставить в покое.

                              Учитывая большую популярность .NET на десктопах вполне возможно, что этот проект не повторит участи SWING.
                              0
                              Xamarin Studio — собсна тот солжнейший пример, который работает :)
                              0
                              Язык разметки планируется? Как будет вызываться OnDraw? В retained режиме или каждый раз для перерисовки содержимого?
                                0
                                Язык разметки планируется, и это может быть как Glade, так и XAML. Неясно, по какому пути пойдут разработчики. OnDraw вызываться не будет, потому что никакой отрисовки в библиотеке нет — только форвардинг на соответствующие интерфейсы существующих тулкитов. Для Canvas OnDraw вызывается каждый раз.
                                0
                                Есть ещё такой проект от независимого разработчика: github.com/picoe/Eto/

                                +поддерживается также WIndows Forms
                                -так как он пишет его один, то неизвестно как проект будет развиваться в дальнейшем.

                                Only users with full accounts can post comments. Log in, please.