Привет всем. Ребят, не могу понять, как вообще можно сделать кастомное меню? Вот к примеру у меня есть UserControl:
<!-- Menu Items -->
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding ElementName=UC, Path=MenuItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:SideMenuItem Style="{Binding ItemContainerStyle, RelativeSource={RelativeSource FindAncestor, AncestorType=UserControl}}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
имеющий в codebehind два свойства зависимости MenuItems( родительская коллекция списка меню ) и ItemContainerStyle( стиль для каждого пункта меню ). Использую это меню вот так:
<local:SideMenu Grid.Column="0" MenuItems="{Binding MenuVM.Categories}">
<local:SideMenu.ItemContainerStyle>
<Style TargetType="{x:Type local:SideMenuItem}">
<Setter Property="NameItem" Value="{Binding Name}"/>
<Setter Property="Children" Value="{Binding Children}"/>
</Style>
</local:SideMenu.ItemContainerStyle>
</local:SideMenu>
У каждого пункта, представляющим класс SideMenuItem, есть тоже два свойства зависимости - Name( заголовок ) и Children( коллекция дочерних элементов меню ).
Все работает нормально. Вот скриншот:
Теперь вопрос в том, как сделать подпункты, чтобы они появлялись по нажатию на родительский пункт относительно него с правой стороны. Типа такого:
Как вообще это делается? Подскажите пожалуйста
Вам не нужно изобретать велосипед, встроенное меню уже есть.
Например, можно сделать так:
<Menu VerticalAlignment="Top" HorizontalAlignment="Left"
Background="{x:Static SystemColors.ControlLightBrush}">
<Menu.ItemsPanel>
<!-- вертикальное расположение для меню верхнего уровня -->
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</Menu.ItemsPanel>
<Menu.ItemContainerStyle>
<!-- центрирование элементов -->
<Style TargetType="MenuItem">
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</Menu.ItemContainerStyle>
<MenuItem Header="Шкатулки"/>
<MenuItem Header="Иконы"/>
<MenuItem Header="Панно">
<MenuItem Header="Fürstenzug"/>
<MenuItem Header="Полтавская баталия"/>
</MenuItem>
<MenuItem Header="Распятия"/>
</Menu>
Единственное, что тут работает не так — подменю открывается вниз, а не вправо.
К сожалению, прямого управления расположением подменю нету. Но это не большая проблема.
Подменю открывается в отдельном Popup
'е. Заглядывая в определение класса MenuItem
, мы видим
[TemplatePart(Name = "PART_Popup", Type = typeof(Popup))]
что означает, что доступ к этому внутреннему элементу можно получить по имени "PART_Popup"
.
Для этого проще всего навесить attached property. Идею и реализацию я стащил из этого ответа.
Кладём attached property:
static class MenuExtensions
{
// стандартное attached property
public static PlacementMode GetMenuPlacement(MenuItem menu) =>
(PlacementMode)menu.GetValue(MenuPlacementProperty);
public static void SetMenuPlacement(MenuItem menu, PlacementMode value) =>
menu.SetValue(MenuPlacementProperty, value);
public static readonly DependencyProperty MenuPlacementProperty =
DependencyProperty.RegisterAttached(
"MenuPlacement",
typeof(PlacementMode),
typeof(MenuExtensions),
new FrameworkPropertyMetadata(PlacementMode.Bottom, OnMenuPlacementChanged));
// вызывается при изменении значения
static void OnMenuPlacementChanged(
DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var menuItem = o as MenuItem;
if (menuItem == null)
return;
if (menuItem.IsLoaded) // загружен => шаблон уже применён
{
UpdatePopupPlacement(menuItem);
}
else // иначе дожидаемся конца загрузки
{
RoutedEventHandler handler = null;
handler = (oo, ee) =>
{
UpdatePopupPlacement(menuItem);
menuItem.Loaded -= handler;
};
menuItem.Loaded += handler;
}
}
static void UpdatePopupPlacement(MenuItem menuItem)
{
if (menuItem.Template.FindName("PART_Popup", menuItem) is Popup popup)
popup.Placement = GetMenuPlacement(menuItem);
}
}
Имея это в своём распоряжении, можно навесить его на MenuItem
:
<MenuItem Header="Панно" local:MenuExtensions.MenuPlacement="Right">
(Ну и чтобы не повторять код, можно поместить это в стиль.)
Результат:
Если нам нужно ещё, к примеру, сдвинуть Popup
, нужно немного дополнить код. Я прорефакторил MenuExtensions
, получилось вот что:
static class MenuExtensions
{
#region attached property PlacementMode MenuPlacement
public static PlacementMode GetMenuPlacement(MenuItem menu) =>
(PlacementMode)menu.GetValue(MenuPlacementProperty);
public static void SetMenuPlacement(MenuItem menu, PlacementMode value) =>
menu.SetValue(MenuPlacementProperty, value);
public static readonly DependencyProperty MenuPlacementProperty =
DependencyProperty.RegisterAttached(
"MenuPlacement",
typeof(PlacementMode),
typeof(MenuExtensions),
new FrameworkPropertyMetadata(
PlacementMode.Bottom,
(o, args) => NowOrOnLoaded(o, UpdatePopupPlacement)));
#endregion
#region attached property Point MenuOffset
public static Point GetMenuOffset(MenuItem menu) =>
(Point)menu.GetValue(MenuOffsetProperty);
public static void SetMenuOffset(MenuItem menu, Point value) =>
menu.SetValue(MenuOffsetProperty, value);
public static readonly DependencyProperty MenuOffsetProperty =
DependencyProperty.RegisterAttached(
"MenuOffset",
typeof(Point),
typeof(MenuExtensions),
new PropertyMetadata(
default(Point),
(o, args) => NowOrOnLoaded(o, UpdatePopupOffset)));
#endregion
static void NowOrOnLoaded(DependencyObject o, Action<MenuItem> a)
{
var menuItem = o as MenuItem;
if (menuItem == null)
return;
if (menuItem.IsLoaded)
{
a(menuItem);
}
else
{
RoutedEventHandler handler = null;
handler = (oo, ee) =>
{
a(menuItem);
menuItem.Loaded -= handler;
};
menuItem.Loaded += handler;
}
}
static void UpdatePopupPlacement(MenuItem menuItem)
{
if (menuItem.Template.FindName("PART_Popup", menuItem) is Popup popup)
popup.Placement = GetMenuPlacement(menuItem);
}
static void UpdatePopupOffset(MenuItem menuItem)
{
if (menuItem.Template.FindName("PART_Popup", menuItem) is Popup popup)
{
var offset = GetMenuOffset(menuItem);
popup.HorizontalOffset = offset.X;
popup.VerticalOffset = offset.Y;
}
}
}
Им можно пользоваться так:
<MenuItem Header="Панно"
local:MenuExtensions.MenuPlacement="Right"
local:MenuExtensions.MenuOffset="15,15"> ...
Обновление: Я обнаружил баг в MenuExtensions
(никогда нельзя доверять чужому коду!), и исправил его. Заодно сложный паттерн с подпиской и отпиской заменил на Task
.
static class MenuExtensions
{
#region attached property PlacementMode MenuPlacement
public static PlacementMode GetMenuPlacement(MenuItem menu) =>
(PlacementMode)menu.GetValue(MenuPlacementProperty);
public static void SetMenuPlacement(MenuItem menu, PlacementMode value) =>
menu.SetValue(MenuPlacementProperty, value);
public static readonly DependencyProperty MenuPlacementProperty =
DependencyProperty.RegisterAttached(
"MenuPlacement",
typeof(PlacementMode),
typeof(MenuExtensions),
new FrameworkPropertyMetadata(
PlacementMode.Bottom,
(o, args) => WhenPopupAvailable(o, UpdatePopupPlacement)));
#endregion
#region attached property Point MenuOffset
public static Point GetMenuOffset(MenuItem menu) =>
(Point)menu.GetValue(MenuOffsetProperty);
public static void SetMenuOffset(MenuItem menu, Point value) =>
menu.SetValue(MenuOffsetProperty, value);
public static readonly DependencyProperty MenuOffsetProperty =
DependencyProperty.RegisterAttached(
"MenuOffset",
typeof(Point),
typeof(MenuExtensions),
new PropertyMetadata(
default(Point),
(o, args) => WhenPopupAvailable(o, UpdatePopupOffset)));
#endregion
static Task TillEvent(
Action<RoutedEventHandler> subscribe, Action<RoutedEventHandler> unsubscribe)
{
var tcs = new TaskCompletionSource<bool>();
RoutedEventHandler handler = null;
handler = (o, e) =>
{
tcs.TrySetResult(true);
unsubscribe(handler);
};
subscribe(handler);
return tcs.Task;
}
static async void WhenPopupAvailable(DependencyObject o, Action<Popup, MenuItem> a)
{
var menuItem = o as MenuItem;
if (menuItem == null)
return;
if (!menuItem.IsLoaded) // не загружен => дожидаемся
await TillEvent(h => menuItem.Loaded += h, h => menuItem.Loaded -= h);
Popup popup = (Popup)menuItem.Template.FindName("PART_Popup", menuItem);
if (popup == null) // нет ещё попапа?
{
var parent = menuItem.Parent as MenuItem;
if (parent == null) // если нет родителя, тогда непонятна причина, выходим
return; // для отладки лучше бросить исключение тут
// если причина в том, что родитель закрыт, подождём пока откроется
if (!parent.IsSubmenuOpen)
await TillEvent(h => parent.SubmenuOpened += h,
h => parent.SubmenuOpened -= h);
}
popup = (Popup)menuItem.Template.FindName("PART_Popup", menuItem);
if (popup != null) // если и тут нету, у нас не вышло его достать
a(popup, menuItem);
}
static void UpdatePopupPlacement(Popup popup, MenuItem menuItem)
{
popup.Placement = GetMenuPlacement(menuItem);
}
static void UpdatePopupOffset(Popup popup, MenuItem menuItem)
{
var offset = GetMenuOffset(menuItem);
popup.HorizontalOffset = offset.X;
popup.VerticalOffset = offset.Y;
}
}
С этим кодом attached property можно навесить не только на «Панно», но и на «Полтавскую баталию».
Виртуальный выделенный сервер (VDS) становится отличным выбором
Я сильно запуталсяWinForms, использую библиотеку Afforge, их пространства имен - AForge
Консоль закрывается после нажатия любой клавиши, ни как не могу понять где ошибка, подскажите кто работал с этой библиотекой
Вся информация с сайта содержится в базе данных XMLКлючевое слово вводится в textbox и при клике на кнопку поиск выпал список данных по ключевому...