WPF: Нестандартное окно / Хабр

Основная проблема

WPF не работает с NC-area.

NC, она же «Non-client area», она же «не-клиентская часть», она же хром, обрабатывается на более низком уровне. Если вам захотелось изменить какой-то из элементов окна — бордюр, иконку, заголовок или кнопку, то первый

, который попадается при поиске — это убрать стиль окна и переделать все самому. Целиком.

За всю историю развития WPF в этом отношении мало что изменилось. К счастью, у меня были исходники из старинного поста Алекса Яхнина по стилизации под Офис 2007, которые он писал работая над демо проектом по популяризации WPF для Микрософта, так что с нуля начинать мне не грозило.

WPF: Нестандартное окно / Хабр

Мне хотелось избежать добавления новых библиотек в проект, но сохранить возможность легко перенести стиль в другое приложение, что и определило такую структуру.

.net 4.0

Помимо реакции на кнопки и иконку, окно должно перемещаться и изменять размер при drag’е за заголовок, за края и уголки. Соответствующие горячие зоны проще всего задать при помощи невидимых контролов. Пример для левого верхнего (северо-западного) угла.


При наличие атрибута Class в ресурсах, методы этого класса можно вызывать просто по имени как обычные обработчики событий, чем мы и воспользовались. Сами обработчики, например MinButtonClick и OnSizeNorthWest, выглядят примерно так:

void MinButtonClick(object sender, RoutedEventArgs e) {
    Window window = ((FrameworkElement)sender).TemplatedParent as Window;
    if (window != null) window.WindowState = WindowState.Minimized;
}

void OnSizeNorthWest(object sender) {
    if (Mouse.LeftButton == MouseButtonState.Pressed) {
        Window window = ((FrameworkElement)sender).TemplatedParent as Window;
        if (window != null && window.WindowState == WindowState.Normal) {
            DragSize(w.GetWindowHandle(), SizingAction.NorthWest);
        }
    }
}

DragSize далее вызывает WinAPI (

) и заставляет Windows перейти в режим измененения размера окна как в до-дотнетовские времена.

.net 4.5

В 4.5 появились удобные классы

. При добавлении к окну, WindowChrome берет на себя функции изменения размера, положения и состояния окна, оставляя нам более «глобальные» проблемы.


При желании, можно использовать WindowChrome и на .Net 4.0, но придется добавить дополнительные библиотеки, например

(спасибо

за подсказку).

Почти готово. Зададим триггеры для контроля изменений интерфейса при изменении состояния окна. Вернемся в XAML и, например, заставим StatusBar’ы изменять цвет в зависимости от значения Window.IsActive.

Обратите внимание, что этот стиль влияет не на темплэйт окна, а на контролы помещенные в наше окно. Помните самый первый фрагмент с пользовательским кодом?

Вот стиль именно этого StatusBar’а мы сейчас и задали. При желании и времени так же можно задать и стиль для других классов контролов, например подправить ScrollBar, чтобы он тоже соответствовал нужному стилю. Но это уже будет упражнение на следующий свободный вечер.

C# и wpf | диалоговые окна

Последнее обновление: 21.03.2022

WPF поддерживает возможность создания модальных диалоговых окон. При вызове модальное окно блокирует доступ к родительскому окну, пока пользователь
не закроет модальное окно.

Для работы добавим в проект новое окно, которое назовем PasswordWindow. Это окно будет выполнять роль модального.

Изменим интерфейс PasswordWindow:

<Window x:Class="WindowApp.PasswordWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WindowApp"
        mc:Ignorable="d"
        Title="Авторизация" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen">
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="20" />
            <RowDefinition Height="20" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBlock>Введите пароль:</TextBlock>
        <TextBox Name="passwordBox" Grid.Row="1" MinWidth="250">Пароль</TextBox>

        <WrapPanel Grid.Row="2" HorizontalAlignment="Right" Margin="0,15,0,0">
            <Button IsDefault="True" Click="Accept_Click" MinWidth="60" Margin="0,0,10,0">OK</Button>
            <Button IsCancel="True" MinWidth="60">Отмена</Button>
        </WrapPanel>

    </Grid>
</Window>

Здесь определено текстовое поле для ввода пароля и две кнопки. Вторая кнопка с атрибутом IsCancel="True" будет выполнять роль отмены.
А первая кнопка будет подтверждать ввод.

Для подтверждения ввода и успешного выхода из модального окна определим в файле кода PasswordWindow определим обработчик первой кнопки Accept_Click:

using System.Windows;

namespace WindowApp
{
    public partial class PasswordWindow : Window
    {
        public PasswordWindow()
        {
            InitializeComponent();
        }

        private void Accept_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = true;
        }

        public string Password
        {
            get { return passwordBox.Text; }
        }

    }
}

Для успешного выхода из модального диалогового окна нам надо для свойства DialogResult установить значение true. Для
второй кнопки необязательно определять обработчик, так как у нее установлен атрибут IsCancel="True", следовательно, ее нажатие будет эквивалентно
результату this.DialogResult = false;. Этот же результат будет при закрытии диалогового окна на крестик.

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

И изменим главную форму MainWindow, чтобы из нее запускать диалоговое окно. Во-первых, определим кнопку:

<Window x:Class="WindowApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WindowApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="300">
    <Grid>
        <Button Width="100" Height="30" Content="Авторизация" Click="Login_Click" />
    </Grid>
</Window>

И определим обработчик для этой кнопки:

private void Login_Click(object sender, RoutedEventArgs e)
{
    PasswordWindow passwordWindow = new PasswordWindow();

    if(passwordWindow.ShowDialog()==true)
    {
        if(passwordWindow.Password=="12345678")
            MessageBox.Show("Авторизация пройдена");
        else
            MessageBox.Show("Неверный пароль");
    }
    else
    {
        MessageBox.Show("Авторизация не пройдена");
    }
}

В итоге при нажатии на кнопку будет отображаться следующее диалоговое окно:

Модальные диалоговые окна в WPF

И в зависимости от результатов ввода будет отображаться то или иное сообщение.

НазадСодержаниеВперед

Wpf mvvm – simple login to an application

Holy long question, Batman!

Q1:
The process would work, I don’t know about using the LoginModel to talk to the MainWindowViewModel however.

You could try something like LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView

I know that singleton’s are considered anti-patterns by some, but I find this to be easiest for situations like these. This way, the singleton class can implement the INotifyPropertyChanged interface and raise events whenever a loginout event is detected.

Implement the LoginCommand on either the LoginViewModel or the Singleton (Personally, I would probably implement this on the ViewModel to add a degree of separation between the ViewModel’s and the “back-end” utility classes). This login command would call a method on the singleton to perform the login.

Q2:
In these cases, I typically have (yet another) singleton class to act as the PageManager or ViewModelManager. This class is responsible for creating, disposing and holding references to the Top-level pages or the CurrentPage (in a single-page only situation).

My ViewModelBase class also has a property to hold the current instance of the UserControl that is displaying my class, this is so I can hook the Loaded and Unloaded events. This provides me the ability to have virtual OnLoaded(), OnDisplayed() and OnClosed() methods that can be defined in the ViewModel so the page can perform loading and unloading actions.

As the MainWindowView is displaying the ViewModelManager.CurrentPage instance, once this instance changes, the Unloaded event fires, my page’s Dispose method is called, and eventually GC comes in and tidy’s up the rest.

Q3:
I’m not sure if I understand this one, but hopefully you just mean “Display login page when user not logged in”, if this is the case, you could instruct your ViewModelToViewConverter to ignore any instructions when the user is not logged in (by checking the SecurityContext singleton) and instead only show the LoginView template, this is also helpful in cases where you want pages that only certain users have rights to see or use where you can check the security requirements before constructing the View, and replacing it with a security prompt.

Sorry for the long answer, hope this helps 🙂

Edit:
Also, you have misspelled “Management”


Edit for questions in comments

How would the LoginManagerSingleton talk directly to the
MainWindowView. Shouldn’t everything go through the
MainWindowViewModel so that there is no code behind on the
MainWindowView

Sorry, to clarify – I don’t mean the LoginManager interacts directly with the MainWindowView (as this should be just-a-view), but rather that the LoginManager just sets a CurrentUser property in response to the call that the LoginCommand makes, which in turn raises the PropertyChanged event and the MainWindowView (which is listening for changes) reacts accordingly.

The LoginManager could then call PageManager.Open(new OverviewScreen()) (or PageManager.Open("overview.screen") when you have IOC implemented) for example to redirect the user to the default screen users see once logged in.

The LoginManager is essentially the last step of the actual login process and the View just reflects this as appropriate.

Also, in typing this, it has occurred to me that rather than having a LoginManager singleton, all this could be housed in the PageManager class. Just have a Login(string, string) method, which sets the CurrentUser on successful log in.

I understand the idea of a PageManagerView, basically through a PageManagerViewModel

I wouldn’t design PageManager to be of View-ViewModel design, just an ordinary house-hold singleton that implements INotifyPropertyChanged should do the trick, this way the MainWindowView can react to the changing of the CurrentPage property.

Is ViewModelBase an abstract class you created?

Yes. I use this class as the base class of all my ViewModel’s.

This class contains

When a logged in detected, CurrentControl is set to a new View

Personally, I would only hold the instance of the ViewModelBase that is currently being displayed. This is then referenced by the MainWindowView in a ContentControl like so: Content="{Binding Source={x:Static vm:PageManager.Current}, Path=CurrentPage}".

I also then use a converter to transform the ViewModelBase instance in to a UserControl, but this is purely optional; You could just rely on ResourceDictionary entries, but this method also allows the developer to intercept the call and display a SecurityPage or ErrorPage if required.

Then when the application starts it detects no one is logged in, and
thus creates a LoginView and sets that to be the CurrentControl.
Rather than harding it that the LoginView is displayed by default

You could design the application so that the first page that is displayed to the user is an instance of the OverviewScreen. Which, since the PageManager currently has a null CurrentUser property, the ViewModelToViewConverter would intercept this and the rather than display the OverviewScreenView UserControl, it would instead show the LoginView UserControl.

If and when the user successfully logs in, the LoginViewModel would instruct the PageManager to redirect to the original OverviewScreen instance, this time displaying correctly as the CurrentUser property is non-null.

How do people get around this limitation as you mention as do others, singletons are bad

I’m with you on this one, I like me a good singleton. However, the use of these should be limited to be used only where necessary. But they do have perfectly valid uses in my opinion, not sure if any one else wants to chime in on this matter though?


Edit 2:

Do you use a publicly available framework/set of classes for MVVM

No, I’m using a framework that I have created and refined over the last twelve months or so. The framework still follows most the MVVM guidelines, but includes some personal touches that reduces the amount of overall code required to be written.

For example, some MVVM examples out there set up their views much the same as you have; Whereas the View creates a new instance of the ViewModel inside of its ViewObject.DataContext property. This may work well for some, but doesn’t allow the developer to hook certain Windows events from the ViewModel such as OnPageLoad().

OnPageLoad() in my case is called after all controls on the page have been created and have come in to view on screen, which may be instantly, within a few minutes after the constructor is called, or never at all. This is where I do most of my data loading to speed up the page loading process if that page has multiple child pages inside tabs that are not currently selected, for example.

But not only that, by creating the ViewModel in this manner increases the amount of code in each View by a minimum of three lines. This may not sound like much, but not only are these lines of code essentially the same for all views creating duplicate code, but the extra line count can add up quite quickly if you have an application that requires many Views. That, and I’m really lazy.. I didn’t become a developer to type code.

What I will do in a future revision through your idea of a page
manager would be to have several views open at once like a tabcontrol,
where a page manager controls pagetabs instead of just a single
userControl. Then tabs can be selected by a separate view binded to
the page manager

In this case, the PageManager won’t need to hold a direct reference to each of the open ViewModelBase classes, only those at the top-level. All other pages will be children of their parent to give you more control over the hierarchy and to allow you to trickle down Save and Close events.

If you put these in an ObservableCollection<ViewModelBase> property in the PageManager, you will only then need to create the MainWindow’s TabControl so that it’s ItemsSource property points to the Children property on the PageManager and have the WPF engine do the rest.

Can you expand a bit more on the ViewModelConverter

Sure, to give you an outline it would be easier to show some code.

    public override object Convert(object value, SimpleConverterArguments args)
    {
        if (value == null)
            return null;

        ViewModelBase vm = value as ViewModelBase;

        if (vm != null && vm.PageTemplate != null)
            return vm.PageTemplate;

        System.Windows.Controls.UserControl template = GetTemplateFromObject(value);

        if (vm != null)
            vm.PageTemplate = template;

        if (template != null)
            template.DataContext = value;

        return template;
    }

Reading through this code in sections it reads:

This is the code in the converter that does most of the grunt work, reading through the sections you can see:

  • Main try..catch block used to catch any class construction errors including,
    • Page does not exist,
    • Run-time error in constructor code,
    • And fatal errors in XAML.
  • convertViewModelTypeToViewType() just tries to find the View that corresponds to the ViewModel and returns the type code that it thinks it should be (this may be null).
  • If this is not null, create a new instance of the type.
  • If we fail to find a View to use, try to create the default page for that ViewModel type. I have a few additional ViewModel base classes that inherit from ViewModelBase that provide a separation of duties between the types of page’s they are.
    • For example, a SearchablePage class will simply display a list of all objects in the system of a certain type and provide the Add, Edit, Refresh and Filter commands.
    • A MaintenancePage will retrieve the full object from the database, dynamically generate and position the controls for the fields that the object exposes, creates children pages based on any collection the object has and provides the Save and Delete commands to use.
  • If we still don’t have a template to use, throw an error so the developer knows something went wrong.
  • In the catch block, any run-time error that occurs are displayed to the user in a friendly ErrorPage.

This all allows me to focus on only creating ViewModel classes as the application will simple display the default pages unless the View pages have been explicitly overridden by the developer for that ViewModel.

Собираем все вместе

Все. Нам осталось только подключить стиль к проекту через ресурсы приложения:


И можно использовать его в любом окне.

WPF: Нестандартное окно / Хабр

— Д.

P.S. Еще раз ссылка на исходники на github’е для тех кто сразу прокрутил вниз ради нее.

Создаем стиль

Стиль для окна, как и для любого другого контрола в WPF задается при помощи ControlTemplate. Содержимое окна будет показываться ContentPresenter’ом, а функциональность которую проще сделать в коде c#, подключится через x:Class атрибут в ResourceDictionary. Все очень стандартно для XAML’а.

Похожее:  Домашняя телефония от интернет-провайдера «Зеленая точка»

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *