PowerShell richtextbox change colour of new line - wpf

Nice and simple question, but a minefield of c# responses on Google that make sense to a PowerShell person like me (I need to learn!)
I have a richtextbox created using XAML, and I'd like to have the ability to add a line in a different coloured font.
Most of the Saipen results suggest $formLogReport.SelectionColor but no such property exists.
I've actually found something that does work but it's overkill and goes beyond what I know about PowerShell - I'm reluctant to use code I don't understand.
http://vcloud-lab.com/entries/powercli/powershell-gui-format-text-on-textbox-and-richtextbox
For reference, the code below uses the function from the link provided.
[void][System.Reflection.Assembly]::LoadWithPartialName( 'presentationframework' )
[void][System.Reflection.Assembly]::LoadWithPartialName( 'System.Windows.Forms' )
[xml]$xaml =  #'
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="RichTextBox Example" Height="530" Width="740" >
<Grid Name="GridName">
<Label Name="SetupLabel" Content="Setup type" FontSize="11" HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="30,25,30,30" Height="25" Width="320" />
<ComboBox Name="SetupList" HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="30,55,30,30" Height="25" Width="320" />
<Label Name="SubsiteLabel" Content="Text in here will be a different size" FontSize="11"
HorizontalAlignment="Left" VerticalAlignment="Top" Margin="30,265,30,30" Height="25" Width="320" />
<TextBox Name="SubsiteBox" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="30,295,0,0"
Height="25" Width="320" TextWrapping="Wrap" TextAlignment="Left" VerticalContentAlignment="Center" />
<Label Name="StuffLabelLabel" Content="Enter Stuff to show up" FontSize="11"
HorizontalAlignment="Left" VerticalAlignment="Top" Margin="30,345,30,30" Height="25" Width="320" />
<TextBox Name="StuffBox" FontSize="11" HorizontalAlignment="Left" VerticalAlignment="Top"
Margin="30,375,0,0" Height="25" Width="320" TextWrapping="Wrap" TextAlignment="Left"
VerticalContentAlignment="Center" />
<Label Name="LogLabel" Content="Log..." HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="11"
Margin="390,25,0,0" Height="25" Width="320" />
<RichTextBox Name="LogReport" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="390,55,0,0"
Height="345" Width="300" >
<FlowDocument>
<Paragraph>"HI THERE"
<Run Text=""/>
</Paragraph>
</FlowDocument>
</RichTextBox>
<Button Name="GoButton" Content="Go!" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="625,435,0,0"
Height="25" Width="65" IsEnabled="False" />
</Grid>
</Window>
'#
$reader = ( New-Object System.Xml.XmlNodeReader $xaml )
try {
$Form = [Windows.Markup.XamlReader]::Load( $reader )
}
catch {
Write-Warning "Unable to parse XML, with error: $( $Error[0] )`n "
}
#===========================================================================
# Load XAML Objects / Form Changes & Conditions
#===========================================================================
$xaml.SelectNodes( "//*[#Name]") | ForEach-Object { Set-Variable -Name "form$( $_.Name )" -Value $Form.FindName( $_.Name ) }
function Format-RichTextBox {
#https://msdn.microsoft.com/en-us/library/system.windows.documents.textelement(v=vs.110).aspx#Propertiesshut
param (
[parameter(Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[System.Windows.Controls.RichTextBox]$RichTextBoxControl,
[String]$Text,
[String]$ForeGroundColor = 'Black',
[String]$BackGroundColor = 'White',
[String]$FontSize = '12',
[String]$FontStyle = 'Normal',
[String]$FontWeight = 'Normal',
[Switch]$NewLine
)
$ParamOptions = $PSBoundParameters
$RichTextRange = New-Object System.Windows.Documents.TextRange( $RichTextBoxControl.Document.ContentEnd, $RichTextBoxControl.Document.ContentEnd )
if ($ParamOptions.ContainsKey('NewLine')) {
$RichTextRange.Text = "`n$Text"
}
else {
$RichTextRange.Text = $Text
}
$Defaults = #{ForeGroundColor='Black';BackGroundColor='White';FontSize='12'; FontStyle='Normal'; FontWeight='Normal'}
foreach ($Key in $Defaults.Keys) {
if ($ParamOptions.Keys -notcontains $Key) {
$ParamOptions.Add($Key, $Defaults[$Key])
}
}
$AllParameters = $ParamOptions.Keys | Where-Object {#('RichTextBoxControl','Text','NewLine') -notcontains $_}
foreach ($SelectedParam in $AllParameters) {
if ($SelectedParam -eq 'ForeGroundColor') {$TextElement = [System.Windows.Documents.TextElement]::ForegroundProperty}
elseif ($SelectedParam -eq 'BackGroundColor') {$TextElement = [System.Windows.Documents.TextElement]::BackgroundProperty}
elseif ($SelectedParam -eq 'FontSize') {$TextElement = [System.Windows.Documents.TextElement]::FontSizeProperty}
elseif ($SelectedParam -eq 'FontStyle') {$TextElement = [System.Windows.Documents.TextElement]::FontStyleProperty}
elseif ($SelectedParam -eq 'FontWeight') {$TextElement = [System.Windows.Documents.TextElement]::FontWeightProperty}
$RichTextRange.ApplyPropertyValue($TextElement, $ParamOptions[$SelectedParam])
}
}
$formstuffbox.Add_KeyDown( {
If ( $args[1].key -eq 'Return' ) {
$formLogReport.AppendText( "$( $formstuffbox.text )`n" )
}
} )
$formsubsitebox.Add_KeyDown( {
If ( $args[1].key -eq 'Return' ) {
Format-RichTextBox -RichTextBoxControl $formLogReport -Text $formsubsitebox.text -ForeGroundColor Red
}
} )
$form.ShowDialog()
Does anyone know of a simpler method? It's only ever going to be used for errors, so only ever needs to become red.

Ok so having spent the best part of 8 hours looking for an easy solution, I decided to churn away at the function already in place and try to dissect what it was doing.
The author Kunal Udapi – to whom I’m very grateful for – has accounted for all possible font changes, for what I want it’s far too much and unnecessary.
To anyone else who came her via a Google result, check out the original code via Github here: https://github.com/kunaludapi/Powershell/blob/master/Powershell%20GUI%20format%20text/Format-TextBlock.ps1
Alternatively my example simplifies it dramatically for just changing colour and can be easily modified for font changes.
[void][System.Reflection.Assembly]::LoadWithPartialName( 'presentationframework' )
[void][System.Reflection.Assembly]::LoadWithPartialName( 'System.Windows.Forms' )
[xml]$xaml = #'
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="RichTextBox Example" Height="530" Width="740" >
<Grid Name="GridName">
<RichTextBox Name="richtextbox1" HorizontalAlignment="Left" Height="232" Margin="66,48,0,0" VerticalAlignment="Top" Width="643">
<FlowDocument>
<Paragraph>
<Run Text="Good morning"/>
</Paragraph>
<Paragraph>
<Run Foreground="#FFD30F0F" FontSize="14" Text="Hi there, how's it going?"/>
</Paragraph>
</FlowDocument>
</RichTextBox>
<TextBox Name="textbox1" HorizontalAlignment="Left" Height="30" Margin="68,313,0,0" TextWrapping="Wrap" Text="TextBox"
VerticalAlignment="Top" Width="540"/>
<Button Name="gobutton1" Content="Button" HorizontalAlignment="Left" Height="30" Margin="634,313,0,0"
VerticalAlignment="Top" Width="75"/>
</Grid>
</Window>
'#
$reader = ( New-Object System.Xml.XmlNodeReader $xaml )
try {
$Form = [Windows.Markup.XamlReader]::Load( $reader )
}
catch {
Write-Warning "Unable to parse XML, with error: $( $Error[0] )`n "
}
#===========================================================================
# Load XAML Objects / Form Changes & Conditions
#===========================================================================
$xaml.SelectNodes( "//*[#Name]" ) | ForEach-Object { Set-Variable -Name "form$( $_.Name )" -Value $Form.FindName( $_.Name ) }
Function WriteRichTextBox {
Param(
[string]$text,
[string]$colour = "Black"
)
$RichTextRange = New-Object System.Windows.Documents.TextRange(
$formrichtextbox1.Document.ContentEnd,$formrichtextbox1.Document.ContentEnd )
$RichTextRange.Text = $text
$RichTextRange.ApplyPropertyValue( ( [System.Windows.Documents.TextElement]::ForegroundProperty ), $colour )
}
$formtextbox1.Add_KeyDown( {
If ( $args[1].key -eq 'Return' ) {
WriteRichTextBox -text "`n$( $formtextbox1.text )" -Colour "Green"
}
} )
$formgobutton1.Add_Click( {
WriteRichTextBox -text "`n$( $formtextbox1.text )"
} )
$form.ShowDialog()

Related

In blazor how can I do a for loop with svg objects?

As an example, on Blazor, I'm trying to do a loop to rotate a rectangle like a hand of a watch. The basic test example is like this:
<svg width="400" height="110">
#for (int i = 1; i < 13; i++)
{
<rect id="rect+#i" width="30" height="60" style="fill:red;stroke-width:3;stroke:rgb(0,0,0)" transform="rotate(#i*30)" />
}
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" transform="rotate(360)" />
</svg>
Whilst you specified the size of the rectangle you did not give it a position.
<svg width="110" height="110" viewBox="-110 -110 220 220">
#for (int i = 0; i < 12; i++)
{
<rect id="rect+#i" x="-15" y="50" width="30" height="60" transform="rotate(#(i*30) 0 0)" />
}
</svg>

Trying to make a for loop to draw an SVG in React

Trying to make a for loop to draw an SVG in react...
I want to draw 4 circle in Svg by using loop
const circlex = 40;
const circley = 40;
<svg>
<foreignObject
className={classes.threeDotIcon}
y="30" width="100" height="100"
>
{
[1, 2, 3, 4].map((data, index) => {
<svg width="100" height="100">
<circle cx={circlex + 1} cy={circley + 1} r="30" stroke="black" fill="#caced5" />
</svg>
})
}
</foreignObject>
</svg>
I need When I Add Svg the circles auto draw in Svg....
Solved.....
<svg>
{
userInRoomLimit.map((data, i) => {
return (
<foreignObject
className={classes.threeDotIcon}
y='30'
x={i * 70}
width="100" height="100"
>
<svg width="100" height="100">
<circle cx='40' cy='40' r="30" stroke="black" fill="#caced5" />
</svg>
</foreignObject>
)
})
}

AvalonDock Not rendering layout when using default layout serialization and deserialization

i am just getting started with AvalonDock and i have been beating my head on the wall for a few days now. I am trying to do the mundane task of loading my layout from the de-serialized dockingmanager. I have attempted what ("AvalonDock DockingManager does not load layout") this post has suggested and am still getting a empty screen.
my wpf application xaml is below:
<Window x:Name="frm_Main" x:Class="DataCAD.Forms.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:aD="http://schemas.xceed.com/wpf/xaml/avalondock"
Title="MainWindow" Height="563.9" Width="832" WindowState="Maximized">
<Grid Margin="0,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="0*"/>
</Grid.ColumnDefinitions>
<Menu HorizontalAlignment="Left" Height="24" VerticalAlignment="Top" Width="{Binding ElementName=frm_Main,Path=ActualWidth}">
<MenuItem Name="MnuI_File" Height="21" Width="35" HeaderStringFormat="" Header="File" >
<MenuItem Name="mnuFile_Importcui" Padding="15,3,3,3" Header="Import Cuix" Click="MnuFile_Importcui_OnClick">
<MenuItem.Icon>
<Image Width="24" Height="24" Source="/Images/cuiImport.png"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Name="mnuFile_Exportcui" Padding="15,3,3,3" Header="Export Cuix" Click="MnuFile_Exportcui_OnClick">
<MenuItem.Icon>
<Image Width="24" Height="24" Source="/Images/cuiExport.png"/>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Name="mnuFile_Exit" Padding="15,3,3,3" Header="Exit" Click="MnuFile_Exit_OnClick"/>
</MenuItem>
<MenuItem Name="MnuI_Edit" Height="21" Width="35" HeaderStringFormat="" Header="Edit" ></MenuItem>
<MenuItem Name="MnuI_View" Height="21" Width="42" HeaderStringFormat="" Header="View" >
<MenuItem Name="mnuView_Cuixexplorer" IsChecked="True" IsCheckable="True" Padding="15,3,3,3" Header="Cuix Explorer" Click="MnuView_Cuixexplorer_OnClick"/>
</MenuItem>
</Menu>
<aD:DockingManager x:Name="Dockman" Margin="0,21,1,0">
<aD:DockingManager.Theme>
<aD:AeroTheme/>
</aD:DockingManager.Theme>
<aD:LayoutRoot>
<aD:LayoutPanel Orientation="Vertical">
<aD:LayoutPanel Orientation="Horizontal">
<aD:LayoutAnchorablePaneGroup DockWidth="150" Orientation="Vertical">
<aD:LayoutAnchorablePane x:Name="CuixExplorerAnchor" DockWidth="150">
<aD:LayoutAnchorable x:Name="CuixExplorer" ContentId="CuixExplorer" Title="Cuix Explorer">
<TreeView Name="trv_CuixDisplay" AllowDrop="True" Drop="trv_CuiDisplay_onDrop">
</TreeView>
</aD:LayoutAnchorable>
</aD:LayoutAnchorablePane>
<aD:LayoutAnchorablePane x:Name="Gen_PropertiesAnchor" DockWidth="150">
<aD:LayoutAnchorable x:Name="Gen_Properties" ContentId="Properties" Title="Properties">
<Grid x:Name="grid"></Grid>
</aD:LayoutAnchorable>
</aD:LayoutAnchorablePane>
</aD:LayoutAnchorablePaneGroup>
<aD:LayoutDocumentPaneGroup Orientation="Vertical" DockWidth="*" x:Name="DocPane">
<aD:LayoutDocumentPane x:Name="DockingMainWindow">
<aD:LayoutDocument ContentId="default" Title="default">
<RichTextBox x:Name="DefaultTextBox"></RichTextBox>
</aD:LayoutDocument>
</aD:LayoutDocumentPane>
</aD:LayoutDocumentPaneGroup>
</aD:LayoutPanel>
<aD:LayoutAnchorablePaneGroup Orientation="Vertical" DockHeight="150" >
<aD:LayoutAnchorablePane DockHeight="150" >
<aD:LayoutAnchorable ContentId="output" Title="Output">
<TextBox x:Name="OutpuTextBox"></TextBox>
</aD:LayoutAnchorable>
</aD:LayoutAnchorablePane>
</aD:LayoutAnchorablePaneGroup>
</aD:LayoutPanel>
</aD:LayoutRoot>
</aD:DockingManager>
</Grid>
My setting xml is below:
<?xml version="1.0" encoding="utf-8"?>
<LayoutRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<RootPanel Orientation="Vertical">
<LayoutPanel Orientation="Horizontal">
<LayoutAnchorablePaneGroup Orientation="Vertical" DockWidth="150">
<LayoutAnchorablePane DockWidth="150">
<LayoutAnchorable AutoHideMinWidth="100" AutoHideMinHeight="100" Title="Cuix Explorer" IsSelected="True" ContentId="CuixExplorer" />
</LayoutAnchorablePane>
<LayoutAnchorablePane DockWidth="150">
<LayoutAnchorable AutoHideMinWidth="100" AutoHideMinHeight="100" Title="Properties" IsSelected="True" ContentId="Properties" />
</LayoutAnchorablePane>
</LayoutAnchorablePaneGroup>
<LayoutDocumentPaneGroup Orientation="Vertical">
<LayoutDocumentPane>
<LayoutDocument Title="default" IsSelected="True" IsLastFocusedDocument="True" ContentId="default" LastActivationTimeStamp="10/05/2013 00:30:01" />
</LayoutDocumentPane>
</LayoutDocumentPaneGroup>
</LayoutPanel>
<LayoutAnchorablePaneGroup Orientation="Vertical" DockHeight="150">
<LayoutAnchorablePane DockHeight="150">
<LayoutAnchorable AutoHideMinWidth="100" AutoHideMinHeight="100" Title="Output" IsSelected="True" ContentId="output" />
</LayoutAnchorablePane>
</LayoutAnchorablePaneGroup>
</RootPanel>
<TopSide />
<RightSide />
<LeftSide />
<BottomSide />
<FloatingWindows />
<Hidden />
</LayoutRoot>
my code is below:
public MainWindow()
{
InitializeComponent();
LoadLayout();
Left = Settings.Default.MAINWINDOW_LEFT;
Top = Settings.Default.MAINWINDOW_TOP;
Width = Settings.Default.MAINWINDOW_WIDTH;
Height = Settings.Default.MAINWINDOW_HEIGHT;
WindowState = (Settings.Default.MAINWINDOW_ISMAXIMIZED) ? WindowState.Maximized : WindowState.Normal;
}
private void MainWindow_OnClosing(object sender , CancelEventArgs cancelEventArgs)
{
Settings.Default.MAINWINDOW_LEFT = Left;
Settings.Default.MAINWINDOW_TOP = Top;
Settings.Default.MAINWINDOW_WIDTH = Width;
Settings.Default.MAINWINDOW_HEIGHT = Height;
Settings.Default.MAINWINDOW_ISMAXIMIZED = (WindowState == WindowState.Maximized);
Settings.Default.CUIXEXPLORER_ISVISIBLE = CuixExplorer.IsVisible;
Settings.Default.CUIXEXPLORER_DOCKLOCATION = CuixExplorer.PreviousContainerIndex;
Settings.Default.CUIXEXPLORER_WIDTH = CuixExplorer.FloatingWidth;
Settings.Default.CUIXEXPLORER_HEIGHT = CuixExplorer.FloatingHeight;
Settings.Default.Save();
SaveLayout();
}
private void SaveLayout()
{
var serializer = new XmlLayoutSerializer(Dockman);
using (var stream = new StreamWriter(_settingsFile))
{
serializer.Serialize(stream);
}
}
private void LoadLayout()
{
var serializer = new XmlLayoutSerializer(Dockman);
using (var stream = new StreamReader(_settingsFile))
{
serializer.Deserialize(stream);
}
}
any help is greatly appreciated. Thanks in advance.
The reason i was not able to Deserialize my layout was because i was missing the Serializer.LayoutSerializationCallback event in my LoadLayout method.
new LoadLayout method:
public void LoadLayout()
{
var serializer = new XmlLayoutSerializer(DockingManagerMain);
// Imparitive for Deserialization
serializer.LayoutSerializationCallback += (s, args) =>
{
args.Content = args.Content;
};
serializer.Deserialize(DockingLayoutConfig);
}
I have come across this issue and the problem is not the callback. It is unnecessary. The method to Deserialize will NOT work before Window_Loaded event happen. If you call it before like in this case in the window constructor the object is not fully loaded therefore cannot load properly the xml. Doing so will cause the white screen issue.
I came upon this error while switching from Syncfusion Docking Manager to Exceed Avalon Dock. I was changing the code for the loading and saving only and not where they are called from. Syncfusion dock manager loading DOES work when called f the constructor and this one obviously is not working there. No big deal, Window_Loaded do the job.
Solution from Franck works with the UserControl_Loaded event in codebehind.
a nicer thing is inherit your viewmodel from Screen and override the OnViewLoaded(object view) method. in your xaml from your view give your dockmanager a x:Name so that you can use it in the OnViewLoaded method.
Something like this (combination of Franck with Trae Moore's code):
protected override void OnViewLoaded(object view)
{
base.OnViewLoaded(view);
MyView v = view as MyView;
if (v?.myDockManager != null) {
var ser = new XmlLayoutSerializer(v.myDockManager);
// ...
}
}

Why is Canvas covering peer controls in Dockpanel?

Why is the Canvas covering the other Children of the Dock Panel?
I'm setting up a menu bar at the top of the client area and a status bar at the bottom of the client area of the window as per standard convention in xaml as follows:
<Window x:Class="RichCoreW.ScenEditWnd"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ScenEditWnd" Height="490" Width="776" HorizontalAlignment="Right">
<DockPanel Name="mapDockP">
<Menu IsMainMenu="True" DockPanel.Dock="Top">
<MenuItem Header="File">
<MenuItem Header="Save" Name="menuISave" Click="menuISave_Click"/>
<MenuItem Header="Make Playable" Click="MakePlayable" />
</MenuItem>
<MenuItem Command="ApplicationCommands.Help" />
</Menu>
<StackPanel DockPanel.Dock="Bottom" Name="stackPanel1" Orientation="Horizontal" background="Yellow">
<Label Content="Playable:" Name="label1" />
<Label Name="labPlayable" />
</StackPanel>
</DockPanel>
</Window>
Then I add a an instance of the MapCanvEdit Class which inherits from Canvas in C# code as follows. As its the last child to be added to the Dockpanel it should take the remaining space in the Dockpanel. But it covers the menu and status bars as well covering the whole of the client area. To be precise it is the children of the Canvas that cover over the other two Stack Panels. Where the Canvas(MapCanvEdit) is empty you can see the Menu and Status bars:
public partial class ScenEditWnd : Window
{
ScenC scenC;
MapCanvEdit mapCanvE;
public ScenEditWnd(ScenC scenCI)
{
InitializeComponent();
scenC = scenCI;
mapCanvE = new MapCanvEdit(scenC);
mapDockP.Children.Add(mapCanvE);
MouseWheel += mapCanvE.Zoom;
mapCanvE.SizeChanged += delegate { mapCanvE.DrawHexs(); };
ContentRendered += delegate { mapCanvE.DrawHexs(); };
labPlayable.Content = scenC.playable.ToString();
}
}
I've left out the other methods for simplicity. Any help appreciated!
It's just the way Canvas works. It can place its children outside its own area. If you want it to restrict children to bounds set ClipToBounds="True" (see ClipToBounds on MSDN) or use another panel.

WPF Popup MenuItem stays highlighted when using arrow keys

After opening a Popup menu programatically, if the user uses up and down arrow keys to move through the menu, menu items get highlighted and they never get unhighlighted. What can I do so that after the user presses the down arrow, the previously highlighted menuitem becomes unhighlighted?
This happens with a very simple Popup menu:
<Grid>
<Button x:Name="Button1" Content="Open Menu"
Click="OnPopupMenuButton_Click"
Height="23" HorizontalAlignment="Left" Margin="69,12,0,0" VerticalAlignment="Top" Width="75" />
<Popup x:Name="MyPopupMenu" StaysOpen="False" >
<StackPanel Orientation="Vertical" Background="White" Margin="0">
<MenuItem x:Name="xAimee" Header="Aimee" Margin="0,2,0,0" />
<MenuItem x:Name="xBarbara" Header="Barbara" />
<MenuItem x:Name="xCarol" Header="Carol" />
<Separator x:Name="xSeparator1" Margin="0,2,2,2"/>
<MenuItem x:Name="xDana" Header="Dana" />
<MenuItem x:Name="xElizabeth" Header="Elizabeth" />
</StackPanel>
</Popup>
</Grid>
Here is how the Popup gets opened:
private void OnPopupMenuButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
Button button = sender as Button;
MyPopupMenu.PlacementTarget = button;
MyPopupMenu.Placement = PlacementMode.Mouse;
MyPopupMenu.IsOpen = true;
MyPopupMenu.StaysOpen = false;
}
I have been following up on archer's suggestion, but I had a few issues. First, I did not want the menu to open on a right-click, partly because I just didn't want it to open on a right-click and partly because I actually need to use PlacementMode.Top, and the context menu kept opening in the standard context-menu place (to the side and down).
So in the end, I did end up using a Context Menu, but I did a couple of special things. First, in the Window constructor, I set the button's ContextMenu to null, to prevent it from opening when right-clicked. Then when the user left-clicks, I programmatically set the ContextMenu to the one that I created in the xaml file. When the menu closes, I set the button's ContextMenu back to null. I tried manipulating the ContextMenu visibility instead, but that did not seem to work as well as setting it to null and back to an object.
Here is the final xaml, not too different from the question exception that I am handling the Closed event for the ContextMenu.
<Button x:Name="xOpenContextMenuButton" Content = "Open Menu"
Click="OnContextMenuButton_Click"
HorizontalAlignment="Right" VerticalAlignment="Bottom"
Width="80" Margin="0,0,36,8" Height="23">
<Button.ContextMenu>
<ContextMenu x:Name="xContextMenu" Closed="OnContextMenu_Closed">
<MenuItem x:Name="xAimee" Header="Aimee" />
<MenuItem x:Name="xBarbara" Header="Barbara" />
<MenuItem x:Name="xCarol" Header="Carol" />
<Separator x:Name="xSeparator1" Margin="0,2,2,2" />
<MenuItem x:Name="xDana" Header="Dana" />
<MenuItem x:Name="xElizabeth" Header="Elizabeth" />
</ContextMenu>
</Button.ContextMenu>
</Button>
Here is the code-behind, which changed a lot:
public MainWindow()
{
InitializeComponent();
xOpenContextMenuButton.ContextMenu = null;
}
private void OnContextMenuButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
xOpenContextMenuButton.ContextMenu = xContextMenu;
xContextMenu.PlacementTarget = xOpenContextMenuButton;
xContextMenu.Placement = PlacementMode.Top;
xContextMenu.IsOpen = true;
xContextMenu.StaysOpen = false;
}
private void OnContextMenu_Closed(object sender, RoutedEventArgs e)
{
xOpenContextMenuButton.ContextMenu = null;
}
Once again, thanks to archer, because I didn't realize that using Popup was not the normal way to create a popup menu in WPF. I think the root cause of the problem is, a Popup can contain anything -- a label, another button, etc. Popup isn't necessarily expecting embedded MenuItems, so it isn't smart enough to understand that it should switch between my menu items when using the arrow keys. But a ContextMenu expects to have MenuItems in it so it knows how to switch between them.

Resources