IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Classe de gestion d'un Popup

Date de mise à jour : 28 mai 2008

Par Anthony DE DECKER (Accueil)
 

Ce tutoriel présente la création d'une classe permettant l'affichage en Popup de n'importe quel type de Control de manière complètement transparente pour celui-ci.

               Version hors-ligne (Miroir)

I. Introduction
I-A. Préalables
I-B. Objectifs
II. Réalisation
II-A. Définition de la classe de "Popupification"
II-A-1. Qu'est réellement notre classe ?
II-A-2. Illustration
II-A-3. Conclusion pour notre classe
II-B. Définition du control mis en Popup et de l'initiateur de l'affichage
II-C. Gestion de la position du Popup
II-D. Définition du style de la fenêtre
II-D-1. Définition du style étendu
II-D-2. Définition du style
II-D-3. Implémentation
II-E. Subclass du control mis en Popup ou de son parent
II-F. Ouverture et fermeture du Popup
II-F-1. Ouverture
II-F-1-a. Gestion de l'activité souris
II-F-1-b. Affichage du Popup
II-F-2. Fermeture
II-F-3. Implémentation
II-G. Gestion des messages systèmes
II-H. Résultat


I. Introduction

Dans un tutoriel précédent ( Création d'un popup ) nous avons vu comment nous pouvions créer et utiliser un Popup. Toutefois, le but de ce tutoriel étant plus de présenter les différentes notions nécessaires, il n'avait pas pour vocation de fournir une solution générique à l'implémentation d'un popup, le composant "popupifié" n'étant pas libéré complètement de tâches inhérentes à la gestion de l'ouverture et de la fermeture du Popup.

Combler cette lacune est l'objectif du présent tutoriel.

Dans celui-ci je vous propose donc de créer une classe qui gérera l'intégralité des tâches propres à la gestion de l'ouverture et de la fermeture du Popup, indépendamment du contenu de ce Popup.


I-A. Préalables

Dans ce tutoriel, nous utiliserons des notions et des classes déjà présentées dans les tutoriels suivants :

Une lecture même succincte de ceux-ci ne pourra que faciliter la compréhension.


I-B. Objectifs

Notre objectif est de permettre à un composant de s'afficher en tant que Popup, sans avoir à gérer les différentes tâches inhérentes à un l'affichage d'un Popup.

Partant du principe que la majorité des composants visuels héritent de la classe Control, nous allons nous limiter à des composants de ce type.

Quelles sont les tâches que devra assurer notre classe que j'appellerai de " Popupification " ?

Elle doit :

  • Positionner le control et l'afficher dans une fenêtre,
  • Activer ou non cette fenêtre (par exemple la DropDownList d'un Combobox n'est pas activée alors qu'un Popup de saisie de texte dans un Propertygrid l'est)
  • Fermer cette fenêtre en cas :
    • De click en dehors de sa zone d'affichage,
    • Sur désactivation de la form parente du control ayant initié son affichage en cas de non activation,
    • Sur sa désactivation si elle a était activée,
    • Sur perte du focus du control ayant initié son affichage dans le cas où celui à gardé l'activité.

II. Réalisation


II-A. Définition de la classe de "Popupification"


II-A-1. Qu'est réellement notre classe ?

Notre classe est une classe d'encapsulation d'un control qui devra "garder un oeil" sur différents messages destinés à ce control (perte d'activation par exemple).

Cela signifie donc qu'elle devra "subclasser" ce control.

Un objet facilitateur du subclassing est la classe System.Windows.Forms.NativeWindow, qui dispose d'une méthode AssignHandle permettant de définir le Handle dont les messages seront interceptés.

Cette classe constituera donc un moyen simple d'effectuer le subclassing du control mis en Popup.


II-A-2. Illustration

Créons une classe SubClassWithNativeWindow héritant de NativeWindow dont l'objectif est de détecter le click gauche de la souris sur un bouton.

Pour ce faire, nous demandons aux instanciateurs de cette classe de fournir dans le constructeur le bouton concerné.

Ensuite, il nous suffit d'assigner le handle du bouton à notre classe et de gérer le message WM_LBUTTONDOWN.

Exemple de subclassing - SubClassWithNativeWindow

	Public Class SubClassWithNativeWindow
	    Inherits System.Windows.Forms.NativeWindow
	
	    Public Sub New(ByVal b As Button)
	        MyBase.AssignHandle(b.Handle)
	    End Sub
	
	    Private Const WM_LBUTTONDOWN As Integer = &H201
	    Protected Overrides Sub WndProc(ByRef m As Message)
	
	        MyBase.WndProc(m)
	
	        If m.Msg = WM_LBUTTONDOWN Then
	            System.Windows.Forms.MessageBox.Show("J'ai vu le click !")
	        End If
	    End Sub
	
	End Class
					

Créons maintenant une Form "FormSubClass" avec un Button "Button1" dont nous demandons la surveillance par une instance de la classe SubClassWithNativeWindow :
Exemple de subclassing - FormSubClass

	Public Class FormSubClass
	    Public Sub New()
	        InitializeComponent()
	        Dim scwnw As New SubClassWithNativeWindow(Me.Button1)
	    End Sub
	End Class
					
Un click gauche de la souris entraînera l'affichage du message "J'ai vu le click !".

Le source de cet exemple est disponible ici : SubClassingExample.zip


II-A-3. Conclusion pour notre classe

Notre classe sera donc héritée de NativeWindow et assurera le subclassing via la procédure WndProc :
Classe CLFWAddPopup - Création

	Option Strict On
	Option Explicit On
	Imports System.ComponentModel
	Imports System.Drawing.Design
	Imports System.Reflection
	Imports System.Windows.Forms
	Imports System.Windows.Forms.Design
	Public Class CLFWAddPopup
	    Inherits System.Windows.Forms.NativeWindow
	
	    Protected Overrides Sub WndProc(ByRef m As Message)
	
	        MyBase.WndProc(m)
	        ' Faire quelque chose ici 
	
	    End Sub
	
	End Class
					

II-B. Définition du control mis en Popup et de l'initiateur de l'affichage

Avant d'afficher un Popup, notre classe doit savoir ce qu'il faut afficher et par qui l'affichage a été déclenché.

Ceci étant un pré-requis, notre classe exigera que ces informations soient passées dans son constructeur (Sub New).
Classe CLFWAddPopup - Constructeur

	#Region "declaration"
	
	    Private cFormParent As Form
	    Private cFormMDIParent As Form
	    Private cPopupControl As Control
	    Private cParentControl As Control
	
	#End Region
	
	Public Sub New(ByVal PopupControl As Control, ByVal ParentControl As Control)
	
	    cPopupControl = PopupControl
	    cParentControl = ParentControl
	
	    If Not cParentControl Is Nothing Then
	        cFormParent = cParentControl.FindForm()
	        cFormMDIParent = cFormParent.MdiParent
	    End If
	
	    If GetType(Form).IsAssignableFrom(cPopupControl.GetType) Then
	        Dim f As Form = CType(cPopupControl, Form)
	        f.FormBorderStyle = FormBorderStyle.None
	        ' Dans le cas présent, un TopLevel à true entraîne la reception 
	        ' de 2 messages de désactivation et bloque la transmission
	        ' du message qui a entrainé la fermeture.
	        ' Il entraîne également l'activation de la fenêtre.
	        f.TopLevel = False
	    End If
	
	End Sub
				
idea Pour simplifier les futurs accès au formulaire parent du control ayant initialisé le Popup ainsi qu'à sa form MDI, nous définissons également dans le constructeur les objets cFormParent et cFormMDIParent.
warning Un control dérivé du type Form mis en Popup ne doit pas être défini comme un control de haut niveau, pour ne pas perturber la gestion des messages d'activation et de désactivation qui seront gérés par notre classe.
C'est pour cela que nous forçons le TopLevel à False.

Nous considérons aussi qu'un contrôle de type form mis en Popup ne devrait pas nécessiter de bordures.

De même, il faudra veiller dans la Form mis en Popup à ne pas utiliser le ShowInTaskbar (lors de l'exécution) car celui-ci provoque la recréation du Handle de la form mis en Popup.
Celle-ci étant subclassée par la suite à partir de son Handle, la recréation du Handle entraînera le dysfonctionnement du subclassing.

II-C. Gestion de la position du Popup

Nous allons permettre un affichage du Popup à différentes positions par rapport au control ayant initié ce Popup.

Pour ce faire, nous commençons par définir une énumération de ces positions :

Positions possibles pour le Popup
Positions possibles du Popup

	Public Enum eCLFWAddPopupPosition
	    BottomLeft = 1
	    BottomRight = 2
	    MiddleLeft = 3
	    MiddleRight = 4
	    TopLeft = 5
	    TopRight = 6
	    BottomLeftCorner = 7
	    BottomRightCorner = 8
	    TopLeftCorner = 9
	    TopRightCorner = 10
	End Enum
				

Et nous mettons à disposition dans notre classe CLFWAddPopup une propriété Position ainsi qu'une procédure privée permettant d'initialiser la position du control qui sera mis en Popup :
Classe CLFWAddPopup - Gestion de la position du Popup

	Private ppPosition As eCLFWAddPopupPosition = eCLFWAddPopupPosition.BottomLeft
	Public Property Position() As eCLFWAddPopupPosition
	    Get
	        Return ppPosition
	    End Get
	    Set(ByVal value As eCLFWAddPopupPosition)
	        ppPosition = value
	    End Set
	End Property
	
	Private Sub SetLocation()
	
	    If cParentControl Is Nothing Then Exit Sub
	
	    Dim pt As New Point
	    pt = cParentControl.PointToScreen(New Point(0, 0))
	
	    Select Case ppPosition
	        Case eCLFWAddPopupPosition.TopLeft
	            pt.Y -= cPopupControl.Height
	        Case eCLFWAddPopupPosition.TopRight
	            pt.X += cParentControl.Width - cPopupControl.Width
	            pt.Y -= cPopupControl.Height
	        Case eCLFWAddPopupPosition.MiddleLeft
	            pt.Y -= CInt((cPopupControl.Height - cParentControl.Height) / 2)
	            pt.X -= cPopupControl.Width
	        Case eCLFWAddPopupPosition.MiddleRight
	            pt.Y -= CInt((cPopupControl.Height - cParentControl.Height) / 2)
	            pt.X += cParentControl.Width
	        Case eCLFWAddPopupPosition.BottomLeft
	            pt.Y += cParentControl.Height
	        Case eCLFWAddPopupPosition.BottomRight
	            pt.X += cParentControl.Width - cPopupControl.Width
	            pt.Y += cParentControl.Height
	        Case eCLFWAddPopupPosition.TopLeftCorner
	            pt.X -= cPopupControl.Width
	            pt.Y -= cPopupControl.Height
	        Case eCLFWAddPopupPosition.TopRightCorner
	            pt.X += cParentControl.Width
	            pt.Y -= cPopupControl.Height
	        Case eCLFWAddPopupPosition.BottomLeftCorner
	            pt.X -= cPopupControl.Width
	            pt.Y += cParentControl.Height
	        Case eCLFWAddPopupPosition.BottomRightCorner
	            pt.X += cParentControl.Width
	            pt.Y += cParentControl.Height
	    End Select
	
	    If Not cFormMDIParent Is Nothing Then
	        pt = cFormMDIParent.PointToClient(pt)
	    End If
	    cPopupControl.Location = pt
	
	End Sub
				

II-D. Définition du style de la fenêtre

Un Popup est avant tout une fenêtre avec un style d'affichage particulier.

Nous allons donc définir une procédure permettant de modifier le style du control "mis en Popup" afin que celui-ci s'affiche dans une fenêtre de style Popup.


II-D-1. Définition du style étendu

Dans le cas, d'un control de type Form nous allons forcer son bit de style étendu à WS_EX_TOOLWINDOW ce qui permettra le non-affichage en barre des tâches indépendamment de la valeur initiale du style étendu (pour ne pas être parasité par ce qui aurait pu être réalisé via le code de l'initialize de la form et pallier la modification du style étendu lors de la modification de la propriété Visible que nous utiliserons par la suite).

Dans le cas d'un control d'un autre type, nous compléterons le bit de style étendu avec WS_EX_TOOLWINDOW afin de conserver l'affichage de sa zone non cliente (la bordure d'un Textbox par exemple).


II-D-2. Définition du style

Nous avons prévu de donner la possibilité d'activer ou non le Popup, c'est à dire activer ou non la fenêtre.

Dans le cas où l'activation à été demandée, nous positionnerons le bit de style à WS_POPUP, ce qui déclenchera cette activation à l'affichage quelque soit le type du control.

Dans le cas contraire nous ne ferons rien car :

  • Si le control mis en Popup est dérivé du type Form, son activation a été bloquée via le TopLevel = False,
  • Si le control mis en Popup ne dérive pas de Form, il ne devrait pas à priori (au custom prés !) bénéficier d'un style déclenchant son activation.

II-D-3. Implémentation

Classe CLFWAddPopup - Définition du style de la fenêtre

	Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" _
	(ByVal hwnd As IntPtr, ByVal nIndex As Integer, ByVal dwNewLong As Integer) As Integer
	Private Declare Function GetWindowLong Lib "user32" _
	Alias "GetWindowLongA" (ByVal hwnd As IntPtr, ByVal nIndex As Integer) As Integer
	
	Private Const WS_POPUP As Integer = &H80000000
	Private Const WS_EX_TOOLWINDOW As Integer = &H80
	Private Const GWL_EXSTYLE As Integer = (-20)
	Private Const GWL_STYLE As Integer = (-16)
	
	Private blnActivate As Boolean
	
	Private Sub SetParams()
	
	    Dim intInitialStyle As Integer
	    Dim intNewStyle As Integer
	
	    ' Style étendu
	    If GetType(Form).IsAssignableFrom(cPopupControl.GetType) Then
	        intNewStyle = WIN32.WS_EX_TOOLWINDOW
	    Else
	        ' Permet de garder la zone NC dans le cas des controls non form
	        intInitialStyle = Win32Api.GetWindowLong(cPopupControl.Handle, WIN32.GWL_EXSTYLE)
	        intNewStyle = intInitialStyle Or WIN32.WS_EX_TOOLWINDOW
	    End If
	    Win32Api.SetWindowLong(cPopupControl.Handle, _
	        WIN32.GWL_EXSTYLE, _
	        intNewStyle)
	
	    ' Style 
	    If blnActivate Then
	        ' Pour permettre l'activation
	        Win32Api.SetWindowLong(cPopupControl.Handle, _
	                WIN32.GWL_STYLE, _
	                WIN32.WS_POPUP)
	    End If
	
	End Sub
					
info Dans la source jointe plus loin, nous ne déclarerons pas les messages et les API directement dans la classe CLFWAddPopup mais dans des Enum publiques et une classe dédiée aux API.
Ils ne sont déclarés ainsi dans le code ci-dessus que pour permettre son utilisation "Stand Alone".

II-E. Subclass du control mis en Popup ou de son parent

Nous allons maintenant définir la procédure déclenchant le subclassing :

  • Du Control mis en Popup dans le cas où celui-ci est activé, ce qui nous permettra de :
    • Réactiver la zone NC (non cliente) du formulaire parent lors de l'activation de la zone NC du Popup (pour éviter que l'activation ait un impact visuel sur la bordure)
    • Déclencher la fermeture du Popup lorsque celui-ci est désactivé.
  • De la Form parente du control ayant initié le Popup dans le cas où celui-ci n'est pas activé afin de déclencher la fermeture du Popup sur désactivation de cette form.
info La fermeture du Popup sur la désactivation, soit du control mis en Popup, soit de la form parente du control ayant initié ce Popup, permet de gérer non seulement la perte d'activité suite à une entrée clavier (type Alt + Tab ou Windows + M) mais également celle induite par un click souris en dehors de l'application.
Classe CLFWAddPopup - Subclassing

	Private Sub SetSubClassing()
	    If blnActivate Then
	        ' En cas d'activation , on subclasse le control mis en popup
	        MyBase.AssignHandle(cPopupControl.Handle)
	    Else
	        ' Sinon, on subclasse la form parente du control ayant 
	        ' initié le Popup
	        MyBase.AssignHandle(cFormParent.Handle)
	    End If
	End Sub				
				

II-F. Ouverture et fermeture du Popup

Nous pouvons maintenant gérer l'ouverture et la fermeture du Popup.


II-F-1. Ouverture

Pour ce faire, nous allons implémenter un procédure publique OpenPopup qui réalisera :

  • L'appel à notre procédure SetParams d'initialisation du style de la fenêtre,
  • L'appel à la procédure SetSubClassing,
  • Le positionnement du parent de la fenêtre du Popup (je vous renvoie à l'article Création d'un popup pour plus de détail),
  • L'abonnement à l'événement de perte de focus du control ayant initié le Popup dans le cas où l'activation du Popup n'a pas été demandée (dans ce cas c'est donc ce control qui reçoit les entrées clavier),
  • L'abonnement à l'événement d'activité souris géré par le filtre de message CLFWAddPreFilter présenté dans l'article Principe du Hook et utilisation d'un Hook souris qui nous permettra de déclencher la fermeture du Popup lors d'un click souris en dehors de l'application.
  • Et enfin l'affichage du Popup.

II-F-1-a. Gestion de l'activité souris

info pour utiliser la class CLFWAddPreFilter, nous ajoutons au préalable la DLL UseMouseHookingDLL mis à disposition dans la source de l'article correspondant.
Nous ne gérerons l'activité de la souris que dans le cas où la fenêtre du Popup n'a pas été activée. En effet, la fenêtre étant activée, la gestion du message de désactivation que nous implémenteront postérieurement sera suffisante.

Lors de l'événement de détection d'un clic de la souris levé par le filtre de message CLFWAddPreFilter, nous controlerons que le clic a été réalisé dans la fenêtre du Popup. Si ce n'est pas le cas, le Popup sera fermé.

warning Ceci à ses limites car certains controls présents dans la fenêtre du Popup sont susceptibles de " déborder " de sa zone client (par exemple la liste d'un combobox) et provoquer donc à tort la fermeture du Popup lors du clic.

II-F-1-b. Affichage du Popup

Pour l'affichage du Popup, nous jouerons simplement sur la propriété .Visible qui nativement déclenchera cette affichage, plutôt que de passer par l'API ShowWindow.

warning Il convient toutefois d'être vigilant car l'affectation de la propriété .Visible à True à un effet sur les bits de style de la fenêtre qui ne se limite pas dans le cas d'une Form à compléter ce bit de style avec WS_VISIBLE.
C'est d'ailleurs pour cela qu'il est nécessaire de différencier la gestion des controls hérités de Form dans la procédure d'initialisation des bits de style (SetParams dans notre classe).

II-F-2. Fermeture

Une procédure publique ClosePopup réalisera le désabonnement aux différents évènements et la fermeture de la fenêtre.

Lors de la fermeture, un événement sera levé pour être utilisé au besoin par le control ayant initié le Popup.


II-F-3. Implémentation

Classe CLFWAddPopup - Gestion de l'ouverture et de la fermeture du Popup

Private Declare Function SetParent Lib "user32" ( _
    ByVal hWndChild As IntPtr, _
    ByVal hWndNewParent As IntPtr) As Integer

    Public Sub OpenPopup(ByVal Activate As Boolean)

        blnActivate = Activate

        SetParams()

        ' Positionnement du parent indispensable
        If cFormMDIParent Is Nothing Then
            SetParent(cPopupControl.Handle, IntPtr.Zero)
        Else
            ' Le popup doit rester dans les limites de la form mdi
            SetParent(cPopupControl.Handle, cFormMDIParent.Handle)
        End If

        If Not blnActivate Then
            ' Perte du focus par le parent
            AddHandler cParentControl.LostFocus, AddressOf ClosePopupStdEvent
            ' Activité souris
            AddHandler CLFWAddPreFilter.Instance.MouseActivity, AddressOf HMouseEvent
        End If

        SetLocation()

        ' Affichage 
        cPopupControl.Visible = True

    End Sub

    Private Sub ClosePopupStdEvent(ByVal sender As Object, ByVal e As EventArgs)
        ClosePopup()
    End Sub

    Public Event PopupClosed()
    Public Sub ClosePopup()

        cPopupControl.Visible = False

        If Not blnActivate Then
            RemoveHandler cParentControl.LostFocus, AddressOf ClosePopupStdEvent
            RemoveHandler CLFWAddMouseHookGlobal.Instance.MouseActivity, AddressOf HMouseEvent
        End If

        RaiseEvent PopupClosed()

    End Sub

    Private Sub HMouseEvent(ByVal e As MouseEventArgs)
        If e.Clicks > 0 Then
            Dim pt As New Point
            pt.X = e.X
            pt.Y = e.Y
            Dim rBounds As Rectangle
            If Not cFormMDIParent Is Nothing Then
                rBounds = cFormMDIParent.RectangleToScreen(cPopupControl.Bounds)
            Else
                rBounds = cPopupControl.Bounds
            End If
            If Not rBounds.Contains(pt) Then Me.ClosePopup()
        End If
    End Sub
				

II-G. Gestion des messages systèmes

Précédemment, nous avons subclassé le Control mis en Popup ou la Form parente du Control ayant initié le Popup.

L'objectif en était de pouvoir détecter les messages d'activation ou d'inactivation pour :

  • Dans le cas où le Popup est activé :
    • Réactiver la zone NC (non cliente) du formulaire parent lors de l'activation de la zone NC (pour éviter que l'activation ait un impact visuel sur la bordure de cette Form),
    • Déclencher la fermeture lors de sa désactivation,
  • Dans le cas où le Popup n'est pas activé, déclencher la fermeture du Popup sur désactivation de la Form parente du Control ayant initié le Popup.
Ceci est réalisé via la procédure WndProc :
Classe CLFWAddPopup - Gestion des messages

	Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageW" _
	(ByVal hWnd As IntPtr, ByVal Msg As Integer, ByVal wParam As Integer, _ 
	ByVal lParam As Integer) As IntPtr
    Protected Overrides Sub WndProc(ByRef m As Message)

        MyBase.WndProc(m)

        '
        ' Si le control mis en popup est activé, on l'a subclassé 
        ' Sinon on a subclassé la form "parente" du popup
        '
        If blnActivate Then
            Select Case m.Msg
                Case WIN32.WM_NCACTIVATE
                    ' Réactivation de la zone NC de la form "parente" lors de la
                    ' perte du message d'activitation de la zone NC du control
                    ' mis en Popup
                    If m.WParam.ToInt32 = 1 Then
                        SendMessage(cFormParent.Handle, WIN32.WM_NCACTIVATE, 1, 0)
                    End If
                Case WIN32.WM_ACTIVATE
                    '
                    ' Fermeture du Popup lors de la désactivation du control mis
                    ' en popup
                    '
                    If m.WParam.ToInt32 = 0 Then
                        ClosePopup()
                    End If
            End Select
        Else
            '
            ' Fermeture du Popup lors de la désactivation de la form parente
            ' du control à l'origine de la création du Popup
            '
            If m.Msg = WIN32.WM_ACTIVATE And m.WParam.ToInt32 = 0 Then
                ClosePopup()
            End If
        End If

    End Sub
				

II-H. Résultat

Notre classe est maintenant terminée et prête à l'emploi.

Etant donné qu'elle sera amenée à être utilisée dans divers projets, nous la positionnons dans un projet de type bibliothèque de classe

src CLFWAddPopupDLL.zip

info La DLL générée dans la source ci-dessus sera celle utilisée dans la suite de l'article.


               Version hors-ligne (Miroir)

Valid XHTML 1.1!Valid CSS!