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.
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 !".
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)
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
f. TopLevel = False
End If
End Sub
|
|
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.
|
|
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
If GetType (Form). IsAssignableFrom (cPopupControl. GetType ) Then
intNewStyle = WIN32. WS_EX_TOOLWINDOW
Else
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)
If blnActivate Then
Win32Api. SetWindowLong (cPopupControl. Handle , _
WIN32. GWL_STYLE , _
WIN32. WS_POPUP )
End If
End Sub
|
|
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.
|
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
MyBase. AssignHandle (cPopupControl. Handle )
Else
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
|
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é.
|
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.
|
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 ()
If cFormMDIParent Is Nothing Then
SetParent (cPopupControl. Handle , IntPtr. Zero )
Else
SetParent (cPopupControl. Handle , cFormMDIParent. Handle )
End If
If Not blnActivate Then
AddHandler cParentControl. LostFocus , AddressOf ClosePopupStdEvent
AddHandler CLFWAddPreFilter. Instance . MouseActivity , AddressOf HMouseEvent
End If
SetLocation ()
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)
If blnActivate Then
Select Case m. Msg
Case WIN32. WM_NCACTIVATE
If m. WParam . ToInt32 = 1 Then
SendMessage (cFormParent. Handle , WIN32. WM_NCACTIVATE , 1, 0)
End If
Case WIN32. WM_ACTIVATE
If m. WParam . ToInt32 = 0 Then
ClosePopup ()
End If
End Select
Else
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
CLFWAddPopupDLL.zip
|
La DLL générée dans la source ci-dessus sera celle utilisée dans
la suite de l'article.
|