Revit: Исправляем проблему неверного отображения содержимого Dockable Panel при масштабировании экрана

Печать

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

Для начала давайте создадим простой проект, который продемонстрирует проблему.

Создаем простую страницу с небольшим количеством содержимого:

<Page x:Class="Revit_FixDockablePanelOnScreenScale.PanelContent"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="clr-namespace:Revit_FixDockablePanelOnScreenScale">

    <Grid Background="White">
        <GroupBox Header="Some group box">
            <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
                <TextBlock FontSize="22" Text="Hello world" HorizontalAlignment="Center"></TextBlock>
                <TextBlock Text="This is some kind of long text that, when the screen is scaled, should go beyond the panel borders"
                           TextWrapping="Wrap"></TextBlock>
            </StackPanel>
        </GroupBox>
    </Grid>
</Page>

Делаем два класса, реализующих интерфейс IExternalApplication – для регистрации панели и IEXternalCommand – для возможности включить или выключить панель:

namespace Revit_FixDockablePanelOnScreenScale
{
    using System;
    using Autodesk.Revit.Attributes;
    using Autodesk.Revit.DB;
    using Autodesk.Revit.UI;
    using Autodesk.Revit.UI.Events;

    public class App : IExternalApplication
    {
        private UIControlledApplication _application;

        public Result OnStartup(UIControlledApplication application)
        {
            _application = application;
            application.Idling += ApplicationOnIdling;

            return Result.Succeeded;
        }

        private void ApplicationOnIdling(object sender, IdlingEventArgs e)
        {
            if (sender is UIApplication uiApp)
            {
                DockablePaneProviderData data = new DockablePaneProviderData();
                PanelContent panelContent = new PanelContent();
                data.InitialState = new DockablePaneState();
                DockablePaneId dockablePaneId = new DockablePaneId(Guid.Parse("0f0f25f5-712d-4b37-ac50-45ed901e77fc"));
                uiApp.RegisterDockablePane(dockablePaneId, "Test Dockable Panel", (IDockablePaneProvider)panelContent);
            }
            else
            {
                _application.Idling -= ApplicationOnIdling;
            }

        }

        public Result OnShutdown(UIControlledApplication application)
        {
            return Result.Succeeded;
        }
    }

    [Regeneration(RegenerationOption.Manual)]
    [Transaction(TransactionMode.Manual)]
    public class Command : IExternalCommand
    {
        public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
        {
            var dpid = new DockablePaneId(Guid.Parse("0f0f25f5-712d-4b37-ac50-45ed901e77fc"));
            if (!DockablePane.PaneIsRegistered(dpid))
            {
                TaskDialog.Show("Test panel", "Not Registered!");
            }
            else
            {
                var dp = commandData.Application.GetDockablePane(dpid);

                if (dp.IsShown())
                    dp.Hide();
                else
                    dp.Show();
            }

            return Result.Succeeded;
        }
    }
}

Я расположил оба класса в одном файле для простоты и удобства в данном случае. Конечно так делать не рекомендуется и каждый класс должен располагаться в отдельном файле!

Собираем проект, создаем addin-файл, кладем в папку с плагинами (это все я не буду описывать) и запускаем Revit. У нас все выглядит хорошо:

Теперь открываем настройки параметров экрана и меняем масштаб на 125%:

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

Важный факт! Когда я писал эту статью, то проводил тесты на Revit 2019 и оказалось, что в нем эту проблему уже пофиксили! Поэтому далее я использовал Revit 2015. Точно могу сказать, что проблема имеется в 2015-2017 и возможно в 2018

Теперь открываем Revit и видим такую картину:

Содержимое у нас уезжает за границу панели. Так вот чтобы решить эту проблему, нам нужно применить обратное масштабирование (т.е. уменьшить) к содержимому панели, но при этом не масштабировать текст. Иначе текст получится слишком мелким по отношению к остальному тексту на экране. Эту идею мне подсказал мой коллега на работе. Также он мне показал решение, которое использовало библиотеку System.Windows.Interactivity.dll. Но мне не понравилось это решение потому что оно было слишком громоздким. Немного поискав похожие темы (не связанные с Revit) в Google я наткнулся на одно подходящее решение с использованием декоратора. К сожалению, ссылку на тот ответ я не сохранил.

Адаптировав решение под нашу конкретную проблему у меня получился вот такой декоратор:

namespace Revit_FixDockablePanelOnScreenScale
{
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;

    public class ScreenScaleDecorator : Decorator
    {
        public ScreenScaleDecorator()
        {
            Loaded += OnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            var presentationSource = PresentationSource.FromVisual(this);
            if (presentationSource != null)
            {
                if (presentationSource.CompositionTarget != null)
                {
                    Matrix m = presentationSource.CompositionTarget.TransformFromDevice;
                    ScaleTransform dpiTransform = new ScaleTransform(m.M11, m.M22);
                    if (dpiTransform.CanFreeze)
                        dpiTransform.Freeze();
                    RenderTransform = dpiTransform;

                    // Масштабируем текст обратно
                    if (Parent is Page page)
                        page.FontSize *= 1 / m.M11;
                }

                // Обязательно нужно отписаться, иначе декоратор будет срабатывать повторно, 
                // когда будут закрыты все документы и открыт новый. Содержимое панели
                // будет масштабироваться в геометрической прогрессии
                Loaded -= OnLoaded;
            }

        }
    }
}

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

<Page x:Class="Revit_FixDockablePanelOnScreenScale.PanelContent"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="clr-namespace:Revit_FixDockablePanelOnScreenScale">
    <local:ScreenScaleDecorator>
        <Grid Background="White">
            <GroupBox Header="Some group box">
                <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
                    <TextBlock FontSize="22" Text="Hello world" HorizontalAlignment="Center"></TextBlock>
                    <TextBlock Text="This is some kind of long text that, when the screen is scaled, should go beyond the panel borders"
                           TextWrapping="Wrap"></TextBlock>
                </StackPanel>
            </GroupBox>
        </Grid>
    </local:ScreenScaleDecorator>
</Page>

Снова запускаем Revit и видим вот такой результат:

Отлично! Проблема исправлена!

Тестовый проект доступен на GitHub

Tags: ,