我有一个Window
包含几个按钮。我需要跟踪按下按钮的时间(MouseDown
) - 开始操作 - 以及何时释放或离开(MouseUp
或MouseLeave
)结束/取消操作。< / p>
取消可能需要一段时间,在此期间我需要阻止用户点击另一个按钮。这是繁忙指示的经典案例。当操作结束时,我可以显示带覆盖的全局忙指示符。然而,可用性方面有一个更好的解决方案,但我正在努力寻找实现它的方法。
1 )初始窗口状态:
2 )按下按钮后,窗口的其余部分应显示为灰色(或模糊效果)并“禁用”。 窗口的其余部分还包括其他几个输入控件(TabControl
,带Button
的通知视图,ToggleButton
等 - 都需要禁用。所以这真的是“所有的孩子,但被点击的Button
”))
3 )当释放按钮时,操作被取消,但由于这可能需要一段时间,因此应该在按钮中显示忙碌指示(我知道怎么做 this < / em>部分)
4 )一旦操作结束,窗口将恢复为初始状态:
Window
上有相同类型的其他按钮(相同的功能和行为)。因此,仅将Panel.ZIndex
设置为常量并不会有效。Window
的其余部分可能不会触发MouseUp
或MouseLeave
等鼠标事件(否则操作会立即取消)。禁用窗口:我已经使用IsEnabled
属性进行了研究。但是,默认情况下,它会传播到所有子元素,并且您无法覆盖一个特定子元素的值。
我实际上找到了一种方法来改变这种行为(这里是关于SO),但我担心改变行为可能会搞砸其他地方的东西(同样,它真的是意料之外的 - 未来的开发人员会认为它很神奇)。此外,我不喜欢控件在禁用状态下看起来如何,并且搞乱这个将是非常有效的。
所以我更喜欢使用某种“叠加”,但它会使背后的东西变灰(我猜color=grey, opacity=0.5
与IsHitTestVisible=True
相结合将是一个开始?)。但问题是我不知道如何在覆盖层顶部获得一个按钮,而窗口的其余部分都留在后面......
编辑:使用ZIndex似乎只适用于同一级别的项目(至少使用网格)。所以这不是一个选择:(
答案 0 :(得分:1)
非常有趣的问题。我试着解决它,如下所示:
使用唯一标识符在我的按钮上定义Tag
属性(我使用了数字,但如果将Tag设置为按钮所做的内容则会有意义)并在下面定义Style
:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
xmlns:converter="clr-namespace:WpfApplication1" Tag="Win" >
<Window.Resources>
<converter:StateConverter x:Key="StateConverter"/>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Value="False">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource StateConverter}">
<Binding Path="ProcessStarter"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</DataTrigger.Binding>
<DataTrigger.Setters>
<Setter Property="Background" Value="White"/>
<Setter Property="Opacity" Value="0.5"/>
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Window.Style>
<Style TargetType="Window">
<Style.Triggers>
<DataTrigger Value="False">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource StateConverter}">
<Binding Path="ProcessStarter"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</DataTrigger.Binding>
<DataTrigger.Setters>
<Setter Property="Background" Value="LightGray"/>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Style>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Tag="1" Content="1" Command="{Binding ActionCommand}" CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
<Button Tag="2" Grid.Column="1" Content="2" Command="{Binding ActionCommand}" CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
<Button Tag="3" Grid.Column="2" Content="3" Command="{Binding ActionCommand}" CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
</Grid>
然后捕获在VM中调用Command的按钮标签,如下所示(此处我已经定义了代码中的所有属性)
public partial class MainWindow : Window, INotifyPropertyChanged
{
private const string _interactiveTags = "1:2:3:Win";
private BackgroundWorker _worker;
public MainWindow()
{
InitializeComponent();
_worker = new BackgroundWorker();
_worker.DoWork += _worker_DoWork;
_worker.RunWorkerCompleted += _worker_RunWorkerCompleted;
ActionCommand = new DelegateCommand(CommandHandler);
DataContext = this;
}
private void CommandHandler(object obj)
{
ProcessStarter = obj.ToString();
if (!_worker.IsBusy)
{
_worker.RunWorkerAsync();
}
}
public ICommand ActionCommand { get; private set; }
void _worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
ProcessStarter = _interactiveTags;
}
void _worker_DoWork(object sender, DoWorkEventArgs e)
{
Thread.Sleep(300);
}
public string _processStarter = _interactiveTags;
public string ProcessStarter
{
get { return _processStarter; }
set
{
_processStarter = value;
RaisePropertyChanged("ProcessStarter");
}
}
最后转换器返回,如果这是提出命令的按钮或正在做某事
public class StateConverter : IMultiValueConverter
{
public string Name { get; set; }
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var tags = (values[0] as string).Split(':');
return tags.Contains(values[1] as string);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
我使用Backgroundworker模拟繁重的工作并使线程休眠300毫秒。测试过它。工作正常。
答案 1 :(得分:1)
因此,经过一些更深思熟虑之后,我认为在装饰层中添加叠加层可能会更好 - 围绕控件。我发现有人已经这样做了,所以我的解决方案很大程度上基于这项工作:http://spin.atomicobject.com/2012/07/16/making-wpf-controls-modal-with-adorners/
这是我的装饰者(如果你有更好的名字,欢迎提出建议!):
public class ElementFocusingAdorner : Adorner
{
private readonly SolidColorBrush WhiteBrush =
new SolidColorBrush(Colors.White);
public ElementFocusingAdorner(UIElement adornedElement)
: base(adornedElement) { }
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.PushOpacity(0.5);
drawingContext.DrawRectangle(WhiteBrush, null, ComputeWindowRect());
base.OnRender(drawingContext);
}
protected override Geometry GetLayoutClip(Size layoutSlotSize)
{
// Add a group that includes the whole window except the adorned control
var group = new GeometryGroup();
group.Children.Add(new RectangleGeometry(ComputeWindowRect()));
group.Children.Add(new RectangleGeometry(new Rect(layoutSlotSize)));
return group;
}
Rect ComputeWindowRect()
{
Window window = Window.GetWindow(AdornedElement);
if (window == null)
{
if (DesignerProperties.GetIsInDesignMode(AdornedElement))
{
return new Rect();
}
throw new NotSupportedException(
"AdornedElement does not belong to a Window.");
}
Point topLeft = window.TransformToVisual(AdornedElement)
.Transform(new Point(0, 0));
return new Rect(topLeft, window.RenderSize);
}
}
此Adorner
需要添加到顶部AdornerLayer
。 Window
有一个(至少是默认ControlTemplate
...)。或者,如果您只想覆盖某个部分,则需要在AdornerLayer
处添加AdornerDecorator
并在UIElement
周围添加Adorner
并添加AdornerLayer
Adorner
那里。
尚未使用:当我在Loaded
事件处理程序中添加<i>
时,未正确绘制装饰器(有点太小)。只要调整窗口大小,装饰就会非常完美。将不得不在这里发布一个问题,以找出导致它的原因......
答案 2 :(得分:0)
经过一番摆弄和大量思考后,我得到了一个有效的解决方案(&#34;原型&#34;):
Rectangle
Opacity=0.5
和Background=White
涵盖整个Window
grid
)Canvas
,也涵盖整个Window
,高于Rectangle
。这用于托管点击的按钮Panel
中删除并添加到Canvas
。
Canvas.Left
,Canvas.Top
,Width
和Height
Panel
(在相同索引处) )。缺少什么:将控件移动到画布然后再返回到面板后,它没有正确调整大小,因为在移动到画布时会设置固定大小。所以基本上当将它移回面板时,需要重置一堆属性以实现与以前相同的行为。
首先,让我们从视图(Window.xaml)开始:
<Window x:Class="PTTBusyIndication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="525"
Height="350">
<Grid>
<UniformGrid Columns="2" Rows="2">
<Button>1</Button>
<Grid Background="LightBlue"
MouseDown="UIElement_OnMouseDown"
MouseLeave="UIElement_OnMouseUpOrLeave"
MouseUp="UIElement_OnMouseUpOrLeave">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
2</TextBlock>
</Grid>
<Button>3</Button>
<Grid Background="LightBlue"
MouseDown="UIElement_OnMouseDown"
MouseLeave="UIElement_OnMouseUpOrLeave"
MouseUp="UIElement_OnMouseUpOrLeave">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
4</TextBlock>
</Grid>
</UniformGrid>
<Rectangle x:Name="overlay"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="White"
Opacity="0.5"
Visibility="Collapsed" />
<Canvas x:Name="overlayContent" />
</Grid>
</Window>
注意:由于这只是一个我不使用MVVM的原型,所以事件处理程序位于MainWindow.xaml.cs
后面的代码中:
public partial class MainWindow : Window
{
private IDisposable _busyElement;
public MainWindow()
{
InitializeComponent();
}
private void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e)
{
if (_busyElement != null)
{
throw new InvalidOperationException("something went wrong, "
+ "there's still a busy element but there shouldn't be!");
}
_busyElement = new TemporarilyMovedElementInfo((FrameworkElement) sender)
.TemporarilyMoveTo(overlayContent);
overlay.Visibility = Visibility.Visible;
}
private void UIElement_OnMouseUpOrLeave(object sender, MouseEventArgs e)
{
if (_busyElement == null)
{
return; // duplicate events because we have up and leave
}
overlay.Visibility = Visibility.Collapsed;
_busyElement.Dispose();
_busyElement = null;
}
}
注意:我已选择使用IDisposable
..许多人可能不喜欢。然而,它清楚地表明它需要被还原(diposed),如果有人没有,我可以让FxCop警告我。 - )。
所以这里是魔法的实现:
public class TemporarilyMovedElementInfo { private readonly FrameworkElement _element; private readonly Panel _originalParent; private readonly Point _originalSize; private readonly Canvas _replacedBy = new Canvas();
public TemporarilyMovedElementInfo(FrameworkElement element)
{
_element = element;
_originalParent = (Panel)element.Parent;
_originalSize = new Point(element.ActualWidth, element.ActualHeight);
}
public IDisposable TemporarilyMoveTo(Canvas canvas)
{
Point positionTxt = GetRelativePositionToWindow(_element);
Point positionCanvas = GetRelativePositionToWindow(canvas);
Point newPosition = new Point(
positionTxt.X - positionCanvas.X,
positionTxt.Y - positionCanvas.Y);
ReplaceChild(_originalParent, _element, _replacedBy);
AddToCanvas(canvas, newPosition);
return new RevertMoveOnDispose(this, canvas);
}
void AddToCanvas(Canvas canvas, Point newPosition)
{
Canvas.SetLeft(_element, newPosition.X);
Canvas.SetTop(_element, newPosition.Y);
_element.Width = _originalSize.X;
_element.Height = _originalSize.Y;
canvas.Children.Add(_element);
}
void MoveBackToOriginalParent(Canvas temporaryParent)
{
temporaryParent.Children.Remove(_element);
ReplaceChild(_originalParent, _replacedBy, _element);
}
void ReplaceChild(Panel panel, UIElement oldElement, UIElement newElement)
{
int index = panel.Children.IndexOf(oldElement);
panel.Children.RemoveAt(index);
panel.Children.Insert(index, newElement);
}
private static Point GetRelativePositionToWindow(Visual v)
{
return v.TransformToAncestor(Application.Current.MainWindow)
.Transform(new Point(0, 0));
}
private class RevertMoveOnDispose : IDisposable
{
private readonly TemporarilyMovedElementInfo _temporarilyMovedElementInfo;
private readonly Canvas _temporaryParent;
public RevertMoveOnDispose(
TemporarilyMovedElementInfo temporarilyMovedElementInfo,
Canvas temporaryParent)
{
_temporarilyMovedElementInfo = temporarilyMovedElementInfo;
_temporaryParent = temporaryParent;
}
public void Dispose()
{
_temporarilyMovedElementInfo.MoveBackToOriginalParent(_temporaryParent);
}
}
}
感谢大家的投入!