Hatena::ブログ(Diary)

遅い→起動時

2013-08-12 (Mon)

WPFでWindowsXPでも使える影付きウィンドウを作ってみた (3)

d:id:pmint:20130810:p1の続き。


ウィンドウの移動とリサイズを独自に実装して、やっと実現できた。



Thickness(30)の影付きウィンドウ。

f:id:pmint:20130812064407p:image


画面端では影部分が細くなる。

f:id:pmint:20130812064405p:image



ウィンドウのBorderを使っているので、ウィンドウサイズを広げた時にBorderThicknessを細くしたりWidthやLeftを調整しなければならない。


でも調整してしまうと…

  • 調整→また移動/リサイズイベント発生→調整のループ。
  • マウス操作で移動やリサイズをすると、一度通常の描画がされてから調整したウィンドウが描画されるため、ウィンドウがブルブル震えて見える。

…といった問題があった。


で、標準的な移動/リサイズの方式ではなく、独自実装に。とはいっても普通にイベントハンドラーを使っている程度。

  • ウィンドウ移動は Window.MouseDown → MouseMove → MouseUp で。
  • リサイズはウィンドウ右下に配置したThumbの DragStarted → DragDelta → DragCompleted で。





実装


プロジェクトファイル(VS2010)

WindowMoving3.2.zip

ClickOnceで実行してみる (Internet Explorer向け)



ウィンドウ定義(XAML)

f:id:pmint:20130813034129p:image



<Window x:Class="WindowMoving3.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"
        WindowStyle="None" Background="WhiteSmoke" BorderThickness="30" AllowsTransparency="True" WindowStartupLocation="CenterScreen"
        Loaded="Window_Loaded" MouseDoubleClick="Window_MouseDoubleClick" StateChanged="Window_StateChanged" Activated="Window_Activated" Deactivated="Window_Deactivated"
        MouseDown="Window_MouseDown" MouseMove="Window_MouseMove" MouseUp="Window_MouseUp"
        Effect="{DynamicResource ResourceKey=dropshadoweffect}">
    <Window.Resources>
        <ControlTemplate x:Key="customthumb" TargetType="{x:Type Thumb}">
            <Viewbox Cursor="SizeNWSE">
                <TextBlock FontFamily="Marlett" Foreground="#80000000">p</TextBlock>
            </Viewbox>
        </ControlTemplate>
        <DropShadowEffect x:Key="dropshadoweffect" Color="Black" BlurRadius="20" ShadowDepth="0" Opacity="0.25" RenderingBias="Performance"></DropShadowEffect>
    </Window.Resources>

    <Grid>
        <Thumb Height="19" HorizontalAlignment="Right" Name="resizeGrip" VerticalAlignment="Bottom" Width="19" Template="{StaticResource ResourceKey=customthumb}"
               DragStarted="resizeGrip_DragStarted" DragDelta="resizeGrip_DragDelta" DragCompleted="resizeGrip_DragCompleted">
        </Thumb>
    </Grid>
</Window>


分離コード(C#)

ここに載せるにはちょっと長いね…。


  • #region AdjustWindow …本体。
  • #region EventHandler …イベントハンドラー。
    Window_…が移動、resizeGrip_…がリサイズのトリガー。


using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using SP = System.Windows.SystemParameters;

namespace WindowMoving3
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        Thickness _initialBorderThickness;

        // 動きをわかりやすくするための画面端の余白
        Thickness _margin = new Thickness(50);

        public MainWindow()
        {
            InitializeComponent();
            this._initialBorderThickness = this.BorderThickness;

            // 動きがわかりやすくなる枠線
            this.BorderBrush = new SolidColorBrush(Color.FromArgb(128, 255, 255, 255));
        }

        #region AdjustWindow ------------------------------------------------------------------------------

        private Size _windowSize;
        private Point _windowOrigin;
        void AdjustWindowStart()
        {
            Debug.WriteLine("AdjustWindowStart");

            var decrease = new Thickness(
                this.BorderThickness.Left - this._initialBorderThickness.Left,
                this.BorderThickness.Top - this._initialBorderThickness.Top,
                this.BorderThickness.Right - this._initialBorderThickness.Right,
                this.BorderThickness.Bottom - this._initialBorderThickness.Bottom);

            this._windowSize = this.RenderSize;
            {
                this._windowSize.Width -= decrease.Left + decrease.Right;
                this._windowSize.Height -= decrease.Top + decrease.Bottom;
            }

            this._windowOrigin = this.RestoreBounds.TopLeft;
            {
                this._windowOrigin.X += decrease.Left;
                this._windowOrigin.Y += decrease.Top;
            }
        }

        void AdjustWindowEnd()
        {
            Debug.WriteLine("AdjustWindowEnd");

            this._windowSize = default(Size);
            this._windowOrigin = default(Point);
        }

        void AdjustWindow(Vector originDelta, Vector sizeDelta)
        {
            // この中でWindow.RestoreBoundsなど(例えばWindow.Left)を参照・変更すると、変更が累積してしまうので不可。変更のみにする。

            Debug.WriteLine(new { originDelta, sizeDelta }, "AdjustWindow");

            var bounds = new Rect(this._windowOrigin, this._windowSize);
            {
                bounds.X += originDelta.X;
                bounds.Y += originDelta.Y;
                bounds.Width = Math.Max(0, this._windowSize.Width + sizeDelta.X);
                bounds.Height = Math.Max(0, this._windowSize.Height + sizeDelta.Y);

                Debug.WriteLine(new { bounds.Left, bounds.Top, bounds.Right, bounds.Bottom, bounds.Width, bounds.Height }, "bounds");
            }

            var over = new Thickness();
            {
                over.Left = Math.Min(this._initialBorderThickness.Left, -Math.Min(0, bounds.Left - (SP.WorkArea.Left + _margin.Left)));
                over.Top = Math.Min(this._initialBorderThickness.Top, -Math.Min(0, bounds.Top - (SP.WorkArea.Top + _margin.Top)));
                over.Right = Math.Min(this._initialBorderThickness.Right, Math.Max(0, bounds.Right - (SP.WorkArea.Right - _margin.Right)));
                over.Bottom = Math.Min(this._initialBorderThickness.Bottom, Math.Max(0, bounds.Bottom - (SP.WorkArea.Bottom - _margin.Bottom)));

                Debug.WriteLine(new { over });
            }

            var thickness = new Thickness(this._initialBorderThickness.Left, this._initialBorderThickness.Top, this._initialBorderThickness.Right, this._initialBorderThickness.Bottom);
            {
                thickness.Left -= over.Left;
                thickness.Right -= over.Right;
                thickness.Top -= over.Top;
                thickness.Bottom -= over.Bottom;

                Debug.WriteLine(new { thickness });
            }

            // 調整するために最低限必要なサイズ
            var newSize = new Size(
                Math.Max(SP.MinimumWindowTrackWidth + (this._initialBorderThickness.Left + this._initialBorderThickness.Right), this._windowSize.Width + sizeDelta.X),
                Math.Max(SP.MinimumWindowTrackHeight + (this._initialBorderThickness.Top + this._initialBorderThickness.Bottom), this._windowSize.Height + sizeDelta.Y));

            this.BorderThickness = thickness;
            this.Width = Limit(
                SP.MinimumWindowTrackWidth,
                newSize.Width - (over.Left + over.Right),
                SP.MaximumWindowTrackWidth);
            this.Left = Math.Max(SP.VirtualScreenLeft, bounds.X + over.Left);
            this.Height = Limit(
                SP.MinimumWindowTrackHeight,
                newSize.Height - (over.Top + over.Bottom),
                SP.MaximumWindowTrackHeight);
            this.Top = Math.Max(SP.VirtualScreenTop, bounds.Y + over.Top);

            Debug.WriteLine(new { this.Left, this.Top, this.Width, this.Height, _windowSizeWidth = this._windowSize.Width + sizeDelta.X, this.BorderThickness });
        }

        public static dynamic Limit(dynamic lower, dynamic current, dynamic upper)
        {
            return Math.Max(lower, Math.Min(upper, current));
        }

        #endregion AdjustWindow

        #region EventHandler ------------------------------------------------------------------------------

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            AdjustWindowStart();
            AdjustWindow(new Vector(0, 0), new Vector(0, 0));
            AdjustWindowEnd();
        }

        void Window_Activated(object sender, EventArgs e)
        {
            this.Effect = (Effect)this.Resources["dropshadoweffect"];
        }

        void Window_Deactivated(object sender, EventArgs e)
        {
            this.Effect = null;
        }

        void Window_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            Debug.WriteLine("Window_MouseDoubleClick");

            if (e.ChangedButton == MouseButton.Left)
            {
                this.WindowState = (this.WindowState != WindowState.Maximized) ? WindowState.Maximized : WindowState.Normal;
                e.Handled = true;   // 最大化→通常に戻したときのために。MouseDown/MouseMoveイベント発生を防ぐ。
            }
        }

        private Thickness _borderThickness;
        void Window_StateChanged(object sender, EventArgs e)
        {
            Debug.WriteLine("Window_StateChanged");

            this._borderThickness = this.BorderThickness;
            switch (this.WindowState)
            {
                case System.Windows.WindowState.Maximized:
                    this.BorderThickness = new Thickness(0);
                    this.resizeGrip.Visibility = Visibility.Collapsed;
                    break;

                default:
                    this.BorderThickness = this._borderThickness;
                    this.resizeGrip.Visibility = Visibility.Visible;
                    break;
            }
        }

        private Point _dragMoveStart;
        void Window_MouseDown(object sender, MouseButtonEventArgs e)
        {
            var c = (Window)sender;
            Debug.WriteLine("Window_MouseDown");

            if (e.LeftButton == MouseButtonState.Pressed && this.WindowState != WindowState.Maximized)
            {
                c.CaptureMouse();
                this._dragMoveStart = c.PointToScreen(e.GetPosition(c));
                AdjustWindowStart();
            }
        }

        void Window_MouseUp(object sender, MouseButtonEventArgs e)
        {
            var c = (Window)sender;
            Debug.WriteLine("Window_MouseUp");

            if (c.IsMouseCaptured)
            {
                AdjustWindowEnd();
                this._dragMoveStart = default(Point);
                c.ReleaseMouseCapture();
            }
        }

        private void Window_MouseMove(object sender, MouseEventArgs e)
        {
            var c = (Window)sender;

            //FIXME:マウスキャプチャー中か否かだけで判定するように
            if (c.IsMouseCaptured && this._dragMoveStart != default(Point))
                AdjustWindow(originDelta: Point.Subtract(c.PointToScreen(e.GetPosition(c)), this._dragMoveStart), sizeDelta: new Vector(0, 0));
        }

        private Vector _dragGripDelta;  // イベントごとの変化量を累積したもの。つまりドラッグ開始からの差分。
        private void resizeGrip_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
        {
            var c = (Thumb)sender;
            Debug.WriteLine("resizeGrip_DragStarted");

            if (this.WindowState != WindowState.Maximized)
            {
                c.CaptureMouse();
                c.Cursor = Cursors.SizeNWSE;
                this._dragGripDelta = new Vector(0, 0);
                AdjustWindowStart();
            }
        }

        private void resizeGrip_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
        {
            var c = (Thumb)sender;
            Debug.WriteLine("resizeGrip_DragCompleted");

            if (c.IsMouseCaptured)
            {
                AdjustWindowEnd();
                this._dragGripDelta = default(Vector);
                c.ReleaseMouseCapture();
            }
        }

        private void resizeGrip_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
        {
            var c = (Thumb)sender;
            Debug.WriteLine("resizeGrip_DragDelta");

            if (c.IsMouseCaptured)
            {
                this._dragGripDelta.X += e.HorizontalChange;
                this._dragGripDelta.Y += e.VerticalChange;
                AdjustWindow(originDelta: new Vector(0, 0), sizeDelta: this._dragGripDelta);
            }
        }

        #endregion EventHandler
    }
}


今後の改善点

  • 右下以外でもウィンドウリサイズができるように…
    ウィンドウを8つのThumbで囲って。
  • コントロールボックス、最小化・最大化・閉じるボタン…
    普通にボタンを配置して。タイトルバーにあたる位置にはなにか適当なコントロールを配置して、ダブルクリックを受け付ければいい。
  • リサイズが重いのをなんとか…
    しなくていい。影部分のDropShadowEffectを外したりBlurRadiusを0にすればいいだけだけど、ウィンドウリサイズに速さはいらない。
    うっすらブラーかけてなおかつ軽くしたいならウィンドウの上にMicrosoft.Windows.Themes.SystemDropShadowChromeを配置、影部分を完全透明にしてChromeのための余白にするといい。

投稿したコメントは管理者が承認するまで公開されません。

トラックバック - http://d.hatena.ne.jp/pmint/20130812/p1