wpf 禁用除单击的控件之外的整个窗口

flvtvl50  于 2023-10-22  发布在  其他
关注(0)|答案(3)|浏览(126)

我有一个Window包含几个按钮。我需要跟踪何时按下按钮(MouseDown)-启动操作-以及何时释放或离开(MouseUpMouseLeave),结束/取消操作。
取消可能需要一段时间,在此期间,我需要防止用户单击另一个按钮。这是忙碌指示的典型情况。我可以在操作结束时显示一个覆盖全局的忙碌指示器。然而,可用性明智的有一个更好的解决方案,但我正在努力寻找一种方法来实现它。

所以我想实现的是:

1)初始窗口状态:

2)一旦按下按钮,窗口的其余部分应该变灰(或模糊效果)并被“禁用”。窗口的其余部分还包括其他几个输入控件TabControl,带有Button的通知视图,ToggleButton等)。--都需要禁用。所以它实际上是“除了被点击的那个Button之外的所有子节点”))

3)当按钮被释放时,操作被取消,但由于这可能需要一段时间,忙碌指示应该显示在按钮中(我知道如何做 * 这 * 部分)

4)一旦操作结束,窗口就会恢复到初始状态:

有两个重要条件:

  • 有相同类型的其他按钮(相同的功能&行为)在同一Window.因此,仅仅将Panel.ZIndex设置为 * 常量 * 是行不通的。
  • 在同一个窗口中有几个其他的输入控件。这些也需要“禁用”。
  • 显示覆盖/灰出其余的Window可能不会触发鼠标事件像MouseUpMouseLeave(否则该操作将立即被取消).

研究完成

关闭窗口:我使用IsEnabled属性进行了研究。但是,默认情况下,它会传播到所有子元素,您不能重写某个特定子元素的值。我实际上已经找到了一种方法来改变这种行为(在SO上),但我担心改变这种行为可能会在其他地方搞砸(而且,这真的是出乎意料的-未来的开发人员会认为它是魔术)。此外,我不喜欢如何控制看起来在禁用状态,并与这将是非常混乱的工作。
所以我更喜欢使用某种“叠加”,但它会使后面的东西变灰(我猜color=grey, opacity=0.5IsHitTestVisible=True结合将是一个开始?但是这里的问题是,我不知道如何让 * 一 * 按钮的顶部覆盖,而所有的窗口的其余部分留在后面.
编辑:使用ZIndex似乎只对同一级别的项目有效(至少在网格中)。所以这也不是一个选择:(

qojgxg4l

qojgxg4l1#

很有趣的问题。我试着像下面这样解决它:
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>

1.然后捕获在VM中调用Command的按钮的Tag,如下所示(这里我已经在后面的代码中定义了所有属性)

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");
    }
}

1.最后是转换器,如果这个按钮引发了命令或正在执行某些操作,

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毫秒。测试过了,工作正常

rkttyhzu

rkttyhzu2#

因此,经过一番深思熟虑,我认为它可能是更好的,只是添加一个覆盖在装饰层-周围的控制。我发现有人已经这样做了,所以我的解决方案在很大程度上基于这项工作: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需要添加到顶部AdornerLayerWindow有一个(至少是它默认的ControlTemplate.)。或者,如果您只想覆盖某个部分,则需要在那里添加AdornerLayer(通过在UIElement周围放置AdornerDecorator),并在那里将Adorner添加到AdornerLayer

**还没有工作:**当我在Loaded事件处理程序中添加Adorner时,装饰器没有正确绘制(有点太小)。一旦窗口调整大小的装饰适合完美。我将不得不在这里提出一个问题,以找出是什么原因造成的。

rkkpypqq

rkkpypqq3#

所以,经过一番折腾和思考,我得到了一个工作解决方案(“原型”):

  • RectangleOpacity=0.5Background=White,覆盖整个Window
  • 它只在单击“按钮”时可见(使用鼠标按下/鼠标向上,它实际上不是一个按钮,而是一个grid
  • Canvas,它也覆盖了整个Window,并且在Rectangle之上。这是用来托管被点击的按钮
  • 当按钮被点击时,它会从Panel中删除并添加到Canvas中。
  • 为了不弄乱“背景”视图,删除的“按钮”需要被另一个项目替换。外表并不重要,因为它将被覆盖。
  • 为了精确地定位在同一点,需要相应地设置Canvas.LeftCanvas.TopWidthHeight
  • 当鼠标按钮被释放(或离开“按钮”区域)按钮从画布中移除并重新添加到它的原始Panel(在相同的索引处)。
  • What's Missing*:在将一个控件移到canvas然后再移回panel后,它的大小没有被正确调整,因为当移到canvas时,设置了一个固定的大小。因此,基本上当将其移回面板时,需要重置一堆属性以实现与以前相同的行为。

首先,让我们从视图(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。很多人可能不喜欢。然而,它清楚地表明,它需要恢复(处置),我可以有FxCop警告我,如果有人不这样做;- ).
下面是魔术的实现:
public class FormData { private; private readonly Panel _originalParent; private readonly Point _originalSize; private readonly Canvas _Bulletin = 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);
        }
    }
}

感谢大家的参与!

相关问题