11using NUnit . Framework ;
2+ using System . Collections . Generic ;
23using System . Linq ;
34using static Vanara . PInvoke . Shell32 ;
45using static Vanara . PInvoke . User32 ;
@@ -8,74 +9,241 @@ namespace Vanara.PInvoke.Tests;
89[ TestFixture ]
910public class ContextMenuTests
1011{
12+ const uint m_CmdFirst = 1 ;
13+
14+ private static IEnumerable < TestCaseData > CreateSources ( )
15+ {
16+ var shi = TestCaseSources . ImageFile ;
17+ ( string ? f , string [ ] i ) [ ] items =
18+ [
19+ ( null , [ ] ) , // Desktop
20+ ( null , [ TestCaseSources . TempDir ] ) , // Folder
21+ ( null , [ shi ] ) , // Single file
22+ ( null , [ shi , TestCaseSources . Image2File ] ) , // Multiple files, same parent
23+ ( null , [ shi , System . IO . Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . Windows ) , "notepad.exe" ) ] ) , // Multiple files, different parents
24+ ( TestCaseSources . TempDir , [ ] ) , // Folder as parent
25+ ( TestCaseSources . TempDir , [ TestCaseSources . TempDir ] ) , // Folder
26+ ( TestCaseSources . TempDir , [ shi ] ) , // Single file
27+ ( TestCaseSources . TempDir , [ shi , TestCaseSources . Image2File ] ) , // Multiple files, same parent
28+ ( @"C:\" , [ shi , System . IO . Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . Windows ) , "notepad.exe" ) ] ) , // Multiple files, different parents
29+ ] ;
30+ foreach ( ( string ? f , string [ ] i ) s in items )
31+ {
32+ foreach ( var e in Enum . GetValues < CMF > ( ) )
33+ {
34+ yield return new TestCaseData ( e , s . f is null ? null : MakeFolder ( s . f ! ) , Array . ConvertAll ( s . i , i => SHCreateItemFromParsingName < IShellItem > ( i ) ) )
35+ . SetArgDisplayNames ( e . ToString ( ) , s . f is null ? "null" : System . IO . Path . GetFileName ( s . f ) , $ "[{ string . Join ( "," , s . i . Select ( i => System . IO . Path . GetFileName ( i ) ) ) } ]") ;
36+ }
37+ }
38+
39+ static IShellFolder MakeFolder ( string path )
40+ {
41+ SHCreateItemHandlerFromParsingName ( path , out IShellFolder ? ppv , BHID . BHID_SFObject ) . ThrowIfFailed ( ) ;
42+ return ppv ! ;
43+ }
44+ }
45+
46+ [ Test ]
47+ public void SHCreateDefaultContextMenuTest ( [ Values ] CMF cmf )
48+ {
49+ Assert . That ( SHParseDisplayName ( TestCaseSources . ImageFile , default , out var pidlChild , 0 , out _ ) , ResultIs . Successful ) ;
50+ Assert . That ( SHParseDisplayName ( System . IO . Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . Windows ) , "notepad.exe" ) , default , out var pidlChild2 , 0 , out _ ) , ResultIs . Successful ) ;
51+ Assert . That ( SHParseDisplayName ( System . IO . Path . GetPathRoot ( TestCaseSources . TempDir ) ! , default , out var pidlFolder , 0 , out _ ) , ResultIs . Successful ) ;
52+ Assert . That ( SHBindToObject ( null , pidlFolder , null , out IShellFolder ? pshf ) , ResultIs . Successful ) ;
53+ //Assert.That(SHGetDesktopFolder(out IShellFolder? pContainingFolder), ResultIs.Successful);
54+
55+ Assert . That ( SHCreateDefaultContextMenu ( new DEFCONTEXTMENU ( pshf ! , [ pidlChild , pidlChild2 ] , null , out _ ) , out IContextMenu3 ? pcm ) , ResultIs . Successful ) ;
56+ Assert . That ( pcm , Is . Not . Null ) ;
57+
58+ using var hmenu = CreatePopupMenu ( ) ;
59+ HRESULT hr ;
60+ Assert . That ( hr = pcm ! . QueryContextMenu ( hmenu , 0 , m_CmdFirst , 0x7FFF , cmf ) , ResultIs . Successful ) ;
61+ TestContext . WriteLine ( $ "Menu items: { hr . Code } ") ;
62+ Assert . That ( hr . Code , Is . GreaterThan ( 0 ) ) ;
63+ var miis = MenuItemInfo . GetMenuItems ( hmenu , pcm ) ;
64+ for ( int i = 0 ; i < miis . Length ; i ++ )
65+ ShowMII ( miis [ i ] , i ) ;
66+ }
67+
68+ [ TestCaseSource ( nameof ( CreateSources ) ) ]
69+ public void SHCreateDefaultContextMenuTest2 ( CMF cmf , IShellFolder ? folder , IShellItem [ ] items )
70+ {
71+ var pcm = SHCreateDefaultContextMenuEx ( folder , out _ , items ) ;
72+ Assert . That ( pcm , Is . Not . Null ) ;
73+
74+ using var hmenu = CreatePopupMenu ( ) ;
75+ HRESULT hr ;
76+ Assert . That ( hr = pcm ! . QueryContextMenu ( hmenu , 0 , m_CmdFirst , 0x7FFF , cmf ) , ResultIs . Successful ) ;
77+ TestContext . WriteLine ( $ "Menu items: { hr . Code } ") ;
78+ Assert . That ( hr . Code , Is . GreaterThan ( 0 ) ) ;
79+ var miis = MenuItemInfo . GetMenuItems ( hmenu , pcm ) ;
80+ for ( int i = 0 ; i < miis . Length ; i ++ )
81+ ShowMII ( miis [ i ] , i ) ;
82+ }
83+
1184 [ Test ]
12- public void QueryTest ( [ Values ] CMF cmf )
85+ public void QueryFolderTest ( [ Values ] CMF cmf )
1386 {
14- var pshi = SHCreateItemFromParsingName < IShellItem > ( TestCaseSources . WordDoc ) ;
87+ var pshi = SHCreateItemFromParsingName < IShellItem > ( TestCaseSources . TempDir ) ;
1588 Assert . That ( pshi , Is . Not . Null ) ;
16- var pcm = pshi ! . BindToHandler < IContextMenu > ( null , BHID . BHID_SFUIObject . Guid ( ) ) ;
89+ var pshf = pshi ! . BindToHandler < IShellFolder > ( null , BHID . BHID_SFObject ) ;
90+ Assert . That ( pshi , Is . Not . Null ) ;
91+ var pcm = pshf . CreateViewObject < IContextMenu > ( HWND . NULL ) ! ;
1792 using var hmenu = CreatePopupMenu ( ) ;
18- Assert . That ( pcm . QueryContextMenu ( hmenu , 0 , 1 , int . MaxValue , cmf ) , ResultIs . Successful ) ;
19- var miis = MenuItemInfo . GetMenuItems ( hmenu ) ;
20- using var memstr = new SafeCoTaskMemString ( 1024 , CharSet . Ansi ) ;
93+ HRESULT hr ;
94+ Assert . That ( hr = pcm . QueryContextMenu ( hmenu , 0 , m_CmdFirst , 0x7FFF , cmf ) , ResultIs . Successful ) ;
95+ TestContext . WriteLine ( $ "Menu items: { hr . Code } ") ;
96+ Assert . That ( hr . Code , Is . GreaterThan ( 0 ) ) ;
97+ var miis = MenuItemInfo . GetMenuItems ( hmenu , pcm ) ;
2198 for ( int i = 0 ; i < miis . Length ; i ++ )
2299 ShowMII ( miis [ i ] , i ) ;
23100 if ( cmf == CMF . CMF_NORMAL )
24101 {
25- var oid = miis . First ( m => m . Verb == "properties" ) . Id ;
26- var cix = new CMINVOKECOMMANDINFOEX ( ( int ) oid - 1 ) ;
27- pcm . InvokeCommand ( cix ) ;
102+ CMINVOKECOMMANDINFOEX cix = new ( new SafeResourceId ( "properties" ) , useUnicode : true ) ;
103+ Assert . That ( pcm . InvokeCommand ( cix ) , ResultIs . Successful ) ;
28104 }
105+ }
29106
30- void ShowMII ( MenuItemInfo mii , int c , int indent = 0 )
107+ [ Test ]
108+ public void QueryItemTest ( [ Values ] CMF cmf )
109+ {
110+ const uint mStartId = 1 ;
111+ var pshi = SHCreateItemFromParsingName < IShellItem > ( TestCaseSources . ImageFile ) ;
112+ Assert . That ( pshi , Is . Not . Null ) ;
113+ var pcm = pshi ! . BindToHandler < IContextMenu > ( null , BHID . BHID_SFUIObject ) ;
114+ using var hmenu = CreatePopupMenu ( ) ;
115+ HRESULT hr ;
116+ Assert . That ( hr = pcm . QueryContextMenu ( hmenu , 0 , mStartId , 0x7FFF , cmf ) , ResultIs . Successful ) ;
117+ TestContext . WriteLine ( $ "Menu items: { hr . Code } ") ;
118+ Assert . That ( hr . Code , Is . GreaterThan ( 0 ) ) ;
119+ var miis = MenuItemInfo . GetMenuItems ( hmenu , pcm ) ;
120+ for ( int i = 0 ; i < miis . Length ; i ++ )
121+ ShowMII ( miis [ i ] , i ) ;
122+ if ( cmf == CMF . CMF_NORMAL )
31123 {
32- mii . Verb = mii . Type == MenuItemType . MFT_STRING && pcm . GetCommandString ( ( IntPtr ) ( int ) ( mii . Id - 1 ) , GCS . GCS_VERBA , default , memstr , memstr . Size ) == HRESULT . S_OK ? memstr . ToString ( ) ?? "" : "" ;
33- TestContext . WriteLine ( $ "{ new string ( ' ' , indent * 3 ) } { c + 1 } ) { mii . Text } (#{ mii . Id } ) - Type={ mii . Type } ; State={ mii . State } ; Verb={ mii . Verb } ") ;
34- for ( int j = 0 ; j < mii . SubMenus . Length ; j ++ )
35- ShowMII ( mii . SubMenus [ j ] , j , indent + 1 ) ;
124+ CMINVOKECOMMANDINFOEX cix = new ( new SafeResourceId ( "properties" ) , useUnicode : true ) ;
125+ Assert . That ( pcm . InvokeCommand ( cix ) , ResultIs . Successful ) ;
36126 }
37127 }
38128
129+ [ Test ]
130+ public void QueryItemsTest ( [ Values ] CMF cmf )
131+ {
132+ const uint mStartId = 1 ;
133+ IShellItem pshi = SHCreateItemFromParsingName < IShellItem > ( TestCaseSources . ImageFile ) ! ;
134+ Assert . That ( pshi , Is . Not . Null ) ;
135+ IShellItem pshi2 = SHCreateItemFromParsingName < IShellItem > ( TestCaseSources . Image2File ) ! ;
136+ Assert . That ( pshi2 , Is . Not . Null ) ;
137+
138+ var pidls = Array . ConvertAll ( [ pshi , pshi2 ] , si => { SHGetIDListFromObject ( si ! , out var pidl ) . ThrowIfFailed ( ) ; return pidl ; } ) ;
139+ var parent = PIDL . FindCommonParent ( pidls ) ;
140+ var relPidls = Array . ConvertAll ( pidls , p => p . GetRelativeTo ( parent ) ) ;
141+ var parentFolder = SHBindToObject < IShellFolder > ( null , parent , null ) ;
142+ var pcm = parentFolder ! . GetUIObjectOf < IContextMenu > ( HWND . NULL , relPidls ) ;
143+
144+ using var hmenu = CreatePopupMenu ( ) ;
145+ HRESULT hr ;
146+ Assert . That ( hr = pcm . QueryContextMenu ( hmenu , 0 , mStartId , 0x7FFF , cmf ) , ResultIs . Successful ) ;
147+ TestContext . WriteLine ( $ "Menu items: { hr . Code } ") ;
148+ Assert . That ( hr . Code , Is . GreaterThan ( 0 ) ) ;
149+ var miis = MenuItemInfo . GetMenuItems ( hmenu , pcm ) ;
150+ for ( int i = 0 ; i < miis . Length ; i ++ )
151+ ShowMII ( miis [ i ] , i ) ;
152+ if ( cmf == CMF . CMF_NORMAL )
153+ {
154+ CMINVOKECOMMANDINFOEX cix = new ( new SafeResourceId ( "properties" ) , useUnicode : true ) ;
155+ Assert . That ( pcm . InvokeCommand ( cix ) , ResultIs . Successful ) ;
156+ }
157+ }
158+
159+ [ Test ]
160+ public void QueryItems2Test ( [ Values ] CMF cmf )
161+ {
162+ const uint mStartId = 1 ;
163+ IShellItem pshi = SHCreateItemFromParsingName < IShellItem > ( TestCaseSources . ImageFile ) ! ;
164+ Assert . That ( pshi , Is . Not . Null ) ;
165+ IShellItem pshi2 = SHCreateItemFromParsingName < IShellItem > ( "C:\\ Windows\\ notepad.exe" ) ! ;
166+ Assert . That ( pshi2 , Is . Not . Null ) ;
167+
168+ var pidls = Array . ConvertAll ( [ pshi , pshi2 ] , si => { SHGetIDListFromObject ( si ! , out var pidl ) . ThrowIfFailed ( ) ; return pidl ; } ) ;
169+ var parent = PIDL . FindCommonParent ( pidls ) ;
170+ var relPidls = Array . ConvertAll ( pidls , p => p . GetRelativeTo ( parent ) ) ;
171+ var parentFolder = SHBindToObject < IShellFolder > ( null , parent , null ) ;
172+ var pcm = parentFolder ! . GetUIObjectOf < IContextMenu > ( HWND . NULL , relPidls ) ;
173+
174+ using var hmenu = CreatePopupMenu ( ) ;
175+ HRESULT hr ;
176+ Assert . That ( hr = pcm . QueryContextMenu ( hmenu , 0 , mStartId , 0x7FFF , cmf ) , ResultIs . Successful ) ;
177+ TestContext . WriteLine ( $ "Menu items: { hr . Code } ") ;
178+ Assert . That ( hr . Code , Is . GreaterThan ( 0 ) ) ;
179+ var miis = MenuItemInfo . GetMenuItems ( hmenu , pcm ) ;
180+ for ( int i = 0 ; i < miis . Length ; i ++ )
181+ ShowMII ( miis [ i ] , i ) ;
182+ if ( cmf == CMF . CMF_NORMAL )
183+ {
184+ CMINVOKECOMMANDINFOEX cix = new ( new SafeResourceId ( "properties" ) , useUnicode : true ) ;
185+ Assert . That ( pcm . InvokeCommand ( cix ) , ResultIs . Successful ) ;
186+ }
187+ }
188+
189+ static void ShowMII ( MenuItemInfo mii , int c , int indent = 0 )
190+ {
191+ if ( mii . Text is "" or "-" )
192+ TestContext . WriteLine ( $ "{ new string ( ' ' , indent * 3 ) } { c + 1 } ) \" { mii . Text } \" (#{ mii . Id } ) - Type={ mii . Type } ; State={ mii . State } ") ;
193+ else
194+ TestContext . WriteLine ( $ "{ new string ( ' ' , indent * 3 ) } { c + 1 } ) \" { mii . Text } \" (#{ mii . Id } ) - Type={ mii . Type } ; State={ mii . State } ; Verb={ mii . Verb } ; Tooltip={ mii . HelpText } ; IconLoc={ mii . VerbIconLocation } ") ;
195+ for ( int j = 0 ; j < mii . SubMenus . Length ; j ++ )
196+ ShowMII ( mii . SubMenus [ j ] , j , indent + 1 ) ;
197+ }
198+
39199 public class MenuItemInfo
40200 {
41- internal MenuItemInfo ( HMENU hMenu , uint idx )
201+ internal MenuItemInfo ( HMENU hMenu , int idx , IContextMenu ? cm )
42202 {
43- using var strmem = new SafeHGlobalHandle ( 512 ) ;
44- var mii = new MENUITEMINFO
203+ // Get the string length
204+ MENUITEMINFO miis = new ( MenuItemInfoMask . MIIM_STRING ) ;
205+ GetMenuItemInfo ( hMenu , ( uint ) Math . Abs ( idx ) , idx >= 0 , ref miis ) ;
206+ using SafeCoTaskMemString strmem = new ( ( int ) miis . cch + 1 , CharSet . Auto ) ;
207+
208+ // Get all the details
209+ MENUITEMINFO mii = new ( MenuItemInfoMask . MIIM_ID | MenuItemInfoMask . MIIM_SUBMENU | MenuItemInfoMask . MIIM_FTYPE | ( miis . cch == 0 ? 0 : MenuItemInfoMask . MIIM_STRING ) | MenuItemInfoMask . MIIM_STATE | MenuItemInfoMask . MIIM_BITMAP )
45210 {
46- cbSize = ( uint ) Marshal . SizeOf ( typeof ( MENUITEMINFO ) ) ,
47- fMask = MenuItemInfoMask . MIIM_ID | MenuItemInfoMask . MIIM_SUBMENU | MenuItemInfoMask . MIIM_FTYPE | MenuItemInfoMask . MIIM_STRING | MenuItemInfoMask . MIIM_STATE | MenuItemInfoMask . MIIM_BITMAP ,
48- fType = MenuItemType . MFT_STRING ,
49211 dwTypeData = ( IntPtr ) strmem ,
50- cch = strmem . Size / ( uint ) StringHelper . GetCharSize ( )
212+ cch = ( uint ) strmem . Capacity
51213 } ;
52- Win32Error . ThrowLastErrorIfFalse ( GetMenuItemInfo ( hMenu , idx , true , ref mii ) ) ;
53- Id = mii . wID ;
54- Text = mii . fType . IsFlagSet ( MenuItemType . MFT_SEPARATOR ) ? "-" : mii . fType . IsFlagSet ( MenuItemType . MFT_STRING ) ? strmem . ToString ( - 1 , CharSet . Auto ) ?? "" : "" ;
214+ Win32Error . ThrowLastErrorIfFalse ( GetMenuItemInfo ( hMenu , ( uint ) Math . Abs ( idx ) , idx >= 0 , ref mii ) ) ;
215+ Id = unchecked ( ( int ) mii . wID ) ;
216+ Text = mii . fType . IsFlagSet ( MenuItemType . MFT_SEPARATOR ) ? "-" : mii . fType . IsFlagSet ( MenuItemType . MFT_STRING ) ? mii . dwTypeData . ToString ( ) ?? "" : "" ;
55217 Type = mii . fType ;
56218 State = mii . fState ;
57219 BitmapHandle = mii . hbmpItem ;
58- SubMenus = GetMenuItems ( mii . hSubMenu ) ;
220+ if ( cm is not null && ! mii . fType . IsFlagSet ( MenuItemType . MFT_SEPARATOR ) )
221+ {
222+ uint id = mii . wID - m_CmdFirst ;
223+ Verb = cm . GetCommandString ( id , GCS . GCS_VERBW , out var mStr ) . Succeeded ? mStr : null ;
224+ HelpText = cm . GetCommandString ( id , GCS . GCS_HELPTEXTW , out mStr ) . Succeeded ? mStr : null ;
225+ VerbIconLocation = cm . GetCommandString ( id , GCS . GCS_VERBICONW , out mStr ) . Succeeded ? mStr : null ;
226+ }
227+ SubMenus = GetMenuItems ( mii . hSubMenu , cm ) ;
59228 }
60229
61- public static MenuItemInfo [ ] GetMenuItems ( HMENU hMenu )
230+ public static MenuItemInfo [ ] GetMenuItems ( HMENU hMenu , IContextMenu ? cm )
62231 {
63- if ( hMenu . IsNull )
64- return new MenuItemInfo [ 0 ] ;
65-
66- var SubMenus = new MenuItemInfo [ GetMenuItemCount ( hMenu ) ] ;
67- for ( uint i = 0 ; i < SubMenus . Length ; i ++ )
68- SubMenus [ i ] = new MenuItemInfo ( hMenu , i ) ;
232+ var SubMenus = new MenuItemInfo [ hMenu . IsNull ? 0 : GetMenuItemCount ( hMenu ) ] ;
233+ for ( int i = 0 ; i < SubMenus . Length ; i ++ )
234+ SubMenus [ i ] = new ( hMenu , i , cm ) ;
69235 return SubMenus ;
70236 }
71237
72- public uint Id { get ; }
238+ public int Id { get ; }
73239 public string Text { get ; }
74240 public MenuItemType Type { get ; }
75241 public MenuItemState State { get ; }
76242 public MenuItemInfo [ ] SubMenus { get ; }
77243 public HBITMAP BitmapHandle { get ; }
78244 public string ? Verb { get ; internal set ; }
245+ public string ? HelpText { get ; internal set ; }
246+ public string ? VerbIconLocation { get ; internal set ; }
79247 }
80248
81249}
0 commit comments