除了单击的控件外,禁用整个窗口

时间:2015-04-24 06:04:56

标签: wpf

我有一个Window包含几个按钮。我需要跟踪按下按钮的时间(MouseDown) - 开始操作 - 以及何时释放或离开(MouseUpMouseLeave)结束/取消操作。< / p>

取消可能需要一段时间,在此期间我需要阻止用户点击另一个按钮。这是繁忙指示的经典案例。当操作结束时,我可以显示带覆盖的全局忙指示符。然而,可用性方面有一个更好的解决方案,但我正在努力寻找实现它的方法。

所以这就是我想要实现的目标:

1 )初始窗口状态: initial window state

2 )按下按钮后,窗口的其余部分应显示为灰色(或模糊效果)并“禁用”。 窗口的其余部分还包括其他几个输入控件TabControl,带Button的通知视图,ToggleButton等 - 都需要禁用。所以这真的是“所有的孩子,但被点击的Button”)) Button pressed - rest of Window disabled

3 )当释放按钮时,操作被取消,但由于这可能需要一段时间,因此应该在按钮中显示忙碌指示(我知道怎么做 this < / em>部分) enter image description here

4 )一旦操作结束,窗口将恢复为初始状态: enter image description here

有两个重要条件:

  • 在同一Window上有相同类型的其他按钮(相同的功能和行为)。因此,仅将Panel.ZIndex设置为常量并不会有效。
  • 同一窗口中还有其他几个输入控件。那些也需要“禁用”。
  • 显示覆盖/灰化Window的其余部分可能不会触发MouseUpMouseLeave等鼠标事件(否则操作会立即取消)。

完成研究

禁用窗口:我已经使用IsEnabled属性进行了研究。但是,默认情况下,它会传播到所有子元素,并且您无法覆盖一个特定子元素的值。 我实际上找到了一种方法来改变这种行为(这里是关于SO),但我担心改变行为可能会搞砸其他地方的东西(同样,它真的是意料之外的 - 未来的开发人员会认为它很神奇)。此外,我不喜欢控件在禁用状态下看起来如何,并且搞乱这个将是非常有效的。

所以我更喜欢使用某种“叠加”,但它会使背后的东西变灰(我猜color=grey, opacity=0.5IsHitTestVisible=True相结合将是一个开始?)。但问题是我不知道如何在覆盖层顶部获得一个按钮,而窗口的其余部分都留在后面......

编辑:使用ZIndex似乎只适用于同一级别的项目(至少使用网格)。所以这不是一个选择:(

3 个答案:

答案 0 :(得分:1)

非常有趣的问题。我试着解决它,如下所示:

  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>
    

  2. 然后捕获在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");
        }
    }
    
  3. 最后转换器返回,如果这是提出命令的按钮或正在做某事

    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();
       }
     }
    
  4. 我使用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需要添加到顶部AdornerLayerWindow有一个(至少是默认ControlTemplate ...)。或者,如果您只想覆盖某个部分,则需要在AdornerLayer处添加AdornerDecorator并在UIElement周围添加Adorner并添加AdornerLayer Adorner那里。

尚未使用:当我在Loaded事件处理程序中添加<i>时,未正确绘制装饰器(有点太小)。只要调整窗口大小,装饰就会非常完美。将不得不在这里发布一个问题,以找出导致它的原因......

答案 2 :(得分:0)

经过一番摆弄和大量思考后,我得到了一个有效的解决方案(&#34;原型&#34;):

  • Rectangle Opacity=0.5Background=White涵盖整个Window
    • 只有在&#34;按钮&#34;单击(使用鼠标按下/鼠标向上,它实际上不是按钮,而是grid
  • Canvas,也涵盖整个Window,高于Rectangle。这用于托管点击的按钮
  • 点击该按钮后,它会从Panel中删除并添加到Canvas
    • 为了不搞乱&#34;背景&#34;查看,删除&#34;按钮&#34;需要被另一个项目替换。看起来并不重要,因为它会被覆盖。
    • 要准确定位于同一地点,需要相应地设置Canvas.LeftCanvas.TopWidthHeight
  • 当释放鼠标按钮(或离开&#34;按钮&#34;区域)时,该按钮将从画布中移除并重新添加到其原始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);
        }
    }
}

感谢大家的投入!