Herencia visual en WPF: Creando una ventana base

jueves, 26 de julio de 2012


Hola a todos

En este post voy a explicar cómo poder tener herencia visual en WPF, pero antes os pongo en antecedentes. En Windows Forms si querías que todas las ventanas de tu aplicación tuvieran el mismo aspecto visual , lo normal era crearse una clase base y que todas las demás heredarán de ella. Cuando se crea una ventana nueva y le indicamos que herede de la ventana base, automáticamente crea el mismo aspecto visual, con los mismos controles que contiene la clase base, colores de fondo etc...

En WPF funciona de otra manera, aquí tenemos los Style y ControlTemplate para conseguir algo parecido, tenemos que crear una clase que herede de Window y definir un estilo que sobrescriba al estilo por defecto de una ventana.


Empecemos con el ejemplo:

Creamos un proyecto de tipo WPF Application, ahora vamos a crearnos una librería donde alojaremos nuestra ventana base y su estilo por defecto.
Lo más sencillo es crear un proyecto de tipo WPF Custom Control Library, ya que de esta manera nos crea la estructura adecuada, con sus referencias ya añadidas y con el AssemblyInfo configurado. Pero vamos a ver qué nos ha creado porque aunque tengamos plantillas y herramientas que nos hacen la vida más fácil, creo que es importante conocer qué es lo que hacen.
Nos ha creado las siguientes referencias para WPF: PresentationFramework, PresentationCore, WindowBase.

En el AssemblyInfo ha creado lo siguiente:

[assembly: ThemeInfo(  ResourceDictionaryLocation.None,   ResourceDictionaryLocation.SourceAssembly)]

Es necesario para que nos encuentre nuestro estilo por defecto personalizado en la ruta por defecto.

Y por ultimo nos ha creado una clase CustomControl1.cs y una carpeta Themes con un fichero Generic.xaml. CustomControl1.cs lo sustituiremos por nuestra ventana base y la estructura Themes/Generic.xaml es la ruta por defecto donde alojaremos nuestro estilo personalizado, los nombres de la ruta deben ser exactamente esos y no se pueden cambiar si queremos no tener que especificarle a la clase base dónde se aloja su estilo, ni tampoco a sus clases derivadas.

Lo que queremos conseguir es una ventana base con un aspecto así:



Sustituimos el UserControl creado por la plantilla por esta clase:

    /// 
    /// Ventana base de tipo lista
    /// 
    public class WindowBase : Window
    {
        /// 
        /// Contructor estático donde se asigna el estilo por defecto
        /// 
        static WindowBase()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(WindowBase), new FrameworkPropertyMetadata(typeof(WindowBase)));
        }
    }

Añadimos un constructor estático y sobre escribimos el estilo por defecto de la ventana.

Ahora vamos a crear el estilo por defecto en el fichero Generic.xaml




    
    
    
        
        
    
    
        
        
    

    
    

    
    
                                            
                                        
                                    
                                    
                                    
                                

                            
                            
                            
                            
                            
                                
                            
                        
                    

                
            
        
    



Lo primero que hemos definido son los colores a utilizar en la ventana. Después hemos definido el estilo que van a utilizar los botones maximizar, minimizar y restaurar. Y por último hemos definido el estilo de la ventana. Lo primero es modificar las propiedades AlowsTransparency y WindowStyle para que la ventana no tenga el borde por defecto de las ventanas de windows. Después hemos modificado el control template de la ventana, que contiene controles de los que se compone nuestra ventana, un borde para la zona del título, con  una imagen y un textblock más los botones, luego un borde para la linea azul y otro borde donde hemos puesto el ContenPresenter, que es el control contenedor donde irá todo lo que en las ventanas derivadas se situe dentro de la propiedad content.

Modificamos nuestra ventana base para que quede así:

    
    /// 
    /// Ventana base de tipo lista
    /// 
    public class WindowBase : Window
    {
        /// 
        /// Contructor estático donde se asigna el estilo por defecto
        /// 
        static WindowBase()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(WindowBase), new FrameworkPropertyMetadata(typeof(WindowBase)));
        }

        /// 
        /// Sobreescribimos cuando se aplica la plantilla para acceder a los controles
        /// y suscribirnos a los eventos segun necesitemos
        /// 
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            #region Minimize, maximize y close
            //accedemos al boton cerrar definido en el estilo
            Button ButtonClose = GetTemplateChild("btClose") as Button;

            if (ButtonClose != null)
            {
                //nos suscribimos al evento click del botón cerrar
                ButtonClose.Click += ButtonClose_Click;

            }

            //accedemos al boton minimizar definido en el estilo
            Button ButtonMinimize = GetTemplateChild("btMinimize") as Button;

            if (ButtonMinimize != null)
            {
                //suscripcion al click del boton minimizar
                ButtonMinimize.Click += ButtonMinimize_Click;
            }

            //accedemos al boton maximizar definido en el estilo
            Button ButtonMaximize = GetTemplateChild("btMaximize") as Button;

            if (ButtonMaximize != null)
            {
                //suscripcion al click del boton maximizar
                ButtonMaximize.Click += ButtonMaximize_Click;
            }
            #endregion

            //accedemos al boton maximizar definido en el estilo
            Border TopBorder = GetTemplateChild("TopBorder") as Border;

            if (TopBorder != null)
            {
                //suscripcion al mousebuttondown del border de la ventana para permitir mover la ventana
                TopBorder.MouseLeftButtonDown += new System.Windows.Input.MouseButtonEventHandler(TopBorder_MouseLeftButtonDown);
            }
        }

        #region Manejadores de eventos de la ventana
            void ButtonClose_Click(object sender, RoutedEventArgs e)
            {
                this.Close();
            }

            void ButtonMinimize_Click(object sender, RoutedEventArgs e)
            {
                this.WindowState = System.Windows.WindowState.Minimized;
            }

            void ButtonMaximize_Click(object sender, RoutedEventArgs e)
            {
                if (this.WindowState == System.Windows.WindowState.Maximized)
                {
                    this.WindowState = System.Windows.WindowState.Normal;
                }
                else
                {
                    this.WindowState = System.Windows.WindowState.Maximized;
                }
            }

            /// 
            /// Manejador del mousedown para poder mover la ventana
            /// 
            /// 
            /// 
            void TopBorder_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
            {
                this.DragMove();
            }
        #endregion

    }
 
Sobreescribimos el método OnApplyTemplate para poder acceder a los botones y al borde definidos en el estilo, para ello hemos tenido que darles un nombre en el estilo. En los Handler que hemos definido para los eventos de los botones y el borde, realizamos las acciones que toque en cada caso, maximizar, minimizar, cerrar o si es el evento del borde, invocamos el evento DragMove que permite mover la ventana.

Bueno pues con esto tenemos nuestra librería de clase base lista, ahora solo nos queda añadirla como referencia en nuestro proyecto principal y modificar a la ventana MainWindow para que herede de nuestra ventana base.

Modificamos el fichero MainWindow.xaml.cs asi:

    public partial class MainWindow : WindowBase
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }


Fijaros que heredamos de nuestra clase base, hemos tenido que hacer el using del namespace donde se encuentre la clase base. Y ahora modificamos el namespace MainWindow.xaml asi:


    

    

    


Ahora lo que hemos hecho es exactamente lo mismo pero en la parte de xaml, hemos añadido el namespace y luego le hemos dicho que herede de la ventana base, pero con sintaxis xaml.

Con esto ya podríamos tener una ventana base para una aplicación, a partir de aquí podríamos ir creando clases base más específicas heredando de WindowBase según tipos de ventana de nuestra aplicación.

Libros Relacionados

WPF 4.5 Unleashed

Pro WPF 4.5 in C#: Windows Presentation Foundation in .NET 4.5 4th Edition (Professional Apress)

9 comentarios:

  1. Hola, ya se que el post es algo antiguo pero me gustaria que respondieras, si es posible que utilizaras Vb.Net ya que no se nada de C# y el tiempo me apremia para aprenderlo. tengo ya mi plantilla en xaml lo que no se es como hacer el codigo para los botones Minimizar, Cerrar.

    ResponderEliminar
    Respuestas
    1. Hola usuario anónimo

      Aparte de una plantilla base para reutilizar el xaml entre ventanas del mismo aspecto también hay una clase base que en el ejemplo llamé WindowBase y es en esta clase base donde se realiza la funcionalidad de cerrar y maximizar.

      Saludos

      Eliminar
  2. Jorge, tratando de buscar información sobre este asunto me he topado con tu interesante artículo. He extraido el código y lo he ido aplicando sobre un proyecto local. El problema es que el código del fichero Generic.xaml parece que no está completo. Lo he retocado y he conseguido compilar los 2 proyectos, pero a pesar de esto, no consigo que funcione. Simplemente parece como que la plantilla no se aplica. También echo en falta los ficheros PNG asociados a los botones.
    ¿Existiría la posibilidad de disponer del código del proyecto elaborado por tí? Me sería de gran interés.
    Muchas gracias.

    ResponderEliminar
    Respuestas
    1. Hola Juan Carlos

      Me alegro que el artículo te este resultando de interesante.

      Hace tiempo que intento publicar en GitHub el código de ejemplo de los post que contienen una parte importante de código, de los post antiguos me queda alguno por actualizar indicando el enlace a GitHub.

      He estado revisando y el código de ejemplo de este post en concreto no lo tengo pero si que tengo el siguiente de la serie sobre herencia en wpf http://xurxodeveloper.blogspot.com.es/2012/08/herencia-visual-en-wpf-creando-una.html, básicamente parto de este ejemplo para hacer algo un poco más especifico que solo una ventana base simple.

      Creo que si te descargas el código de este segundo artículo es fácil extraer solo la parte de este primer artículo.

      Espero que te pueda servir.

      Saludos

      Eliminar
    2. Muchas gracias Jorge, y también por tu rápida respuesta.

      Por cierto, ¿dónde se aprende esto? :-)

      Eliminar
    3. Juan Carlos, dedicando mucho tiempo, recuerdo que cuando me surgió la necesidad dedique mucho tiempo a buscar información donde pude porque me negaba a repetir lo mismo en todas las ventanas, como me costó tanto dar con la solución me pareció una buena idea compartirlo.

      Saludos

      Eliminar
  3. Puedes mostrar la manera de Hacerlo con un metodo POST?

    ResponderEliminar
  4. PANDYHK no te entiendo, no estoy haciendo ninguna petición http, todo el artículo tiene que ver con parte cliente utilizando WPF y Xaml

    ResponderEliminar
  5. Hola Jorge. No sé si es por el tiempo que a pasado o por cambios inesperados en tu servidor. Pero, haciendo tu interesante código, está muy mal escrito. Me sorprende que alguien no te lo hubiera reportado. En la versión de visual que estoy utilizando (2015), un RadialGradientBrush no se puede poner dentro de un SolidColorBrush. En el botón Minimizar usas Image y en Maximizar "img" que no existe. Hay un completo descuido en cerrar llaves (en el botón Minimizar no cierras nunca Image.Resources, y de allí para abajo se vuelve desalentador seguirte el hilo. Muy interesante el artículo, pero la transcipción del código es tenaz.

    ResponderEliminar