none
在處理ManipulationDelta時, 如何將Image或是Grid限制在ScrollViewer或是螢幕某個範圍內? RRS feed

  • 问题

  • 現在我可以用ScrollViewer裡面的grid來做拖拉, 但是, 不知道要如何把Grid限制在螢幕某個區域內? 例如, 放大之後, 在移動時不讓Grid超出螢幕.

    下面為相關的程式碼.

    在ScrollViewer裡面放了一個Grid, Grid裡面裡面放兩個Image, XAML如下:

        <Grid x:Name="grid"  Background="Black"  >
            <ScrollViewer x:Name="scroll_pdf" Background="Black" HorizontalScrollBarVisibility="Hidden" 
                          VerticalScrollBarVisibility="Hidden" 
                          RenderTransformOrigin="0.5,0.5" ViewChanged="scroll_pdf_ViewChanged"
                          Tapped="DetectPosition"
                          ZoomMode="Enabled" MaxZoomFactor="2.6" MinZoomFactor="0.8"
                           >
                <Grid x:Name="imgGrid" ScrollViewer.HorizontalScrollBarVisibility="Hidden"  
                     ScrollViewer.VerticalScrollBarVisibility="Hidden">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Image Name ="img1" Grid.Column="0"  Stretch="UniformToFill" >
                     </Image>
                    <Image Name ="img2" Grid.Column="1" Stretch="UniformToFill" >
                     </Image>
                </Grid>
            </ScrollViewer>
        </Grid>

    另外, 參考微軟關於Manipulation sample (http://code.msdn.microsoft.com/windowsapps/Input-3dff271b), 也加入相關的Manipulation的event handler.

                    imgGrid.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY
                                               | ManipulationModes.Scale | ManipulationModes.ScaleInertia
                                               | ManipulationModes.TranslateRailsX | ManipulationModes.TranslateRailsY;
                    imgGrid.ManipulationStarting += new ManipulationStartingEventHandler(imgGrid_ManipulationStarting);
                    imgGrid.ManipulationStarted += new ManipulationStartedEventHandler(imgGrid_ManipulationStarted);
                    imgGrid.ManipulationDelta += new ManipulationDeltaEventHandler(imgGrid_ManipulationDelta);
                    imgGrid.ManipulationCompleted += new ManipulationCompletedEventHandler(imgGrid_ManipulationCompleted);
                    InitManipulationTransforms();

    其中的 imgGrid_ManipulationDelta 程式碼如下: 

           void imgGrid_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
            {
                if (_forceManipulationsToEnd || scroll_pdf.ZoomFactor <=1)
                {
                    e.Complete();
                    return;
                }
                var newTranslateX = _compositeTransform.TranslateX + e.Delta.Translation.X;
                var newTranslateY = _compositeTransform.TranslateY + e.Delta.Translation.Y;
                _compositeTransform.TranslateX = e.Delta.Translation.X;  
               _compositeTransform.TranslateY = e.Delta.Translation.Y;  
                e.Handled = true;
            }



    2013年1月25日 3:14

全部回复

  • Hi,

    我给你写了一个例子。在这里我发现限制移动并不是很困难的问题,但是当有放大缩小尤其是在键盘上使用Ctrl键的放大缩小的时候,问题就变得比较复杂。因此我希望能够找到一个针对所有情况的解决方案,我发现在键盘上使用Ctrl键进行的放大缩小无论是e.Delta.Scale或者是Height,ActualHeight,Measure()方法都检测不到,只有TranformToVisual()方法可以检测到这个放大缩小,而且这个在使用手势的时候也可以正常工作,因此基于这个方法得到的Grid的数据,我写了这个例子:

    XAML:

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition Height="20" />
                <RowDefinition Height="20" />
            </Grid.RowDefinitions>
            <ScrollViewer Name="scrollViewer" Grid.Row="0" Height="600" Width="600" Background="Aqua" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">
                <Grid Name="imgGrid" ManipulationMode="All" ManipulationDelta="imgGrid_ManipulationDelta_1" Background="Red" Height="100" Width="100">
                    <Grid.RenderTransform>
                        <CompositeTransform x:Name="transform" />
                    </Grid.RenderTransform>
                </Grid>
            </ScrollViewer>
    
            <TextBlock Grid.Row="1" Text="{Binding ElementName=transform,Path=TranslateX}" />
            <TextBlock Grid.Row="2" Text="{Binding ElementName=transform,Path=TranslateY}"/>
    
        </Grid>

     private void imgGrid_ManipulationDelta_1(object sender, ManipulationDeltaRoutedEventArgs e)
            {
                var trans = this.imgGrid.TransformToVisual(this.scrollViewer);
                Point point = trans.TransformPoint(new Point(0, 0));
                Rect rect = trans.TransformBounds(new Rect(0, 0, 1, 1));
    
                double height = this.imgGrid.ActualHeight * rect.Height;
                double width = this.imgGrid.ActualWidth * rect.Width;
    
                double maxTranslateX = this.scrollViewer.ActualWidth - point.X - width + this.transform.TranslateX;
                double minTranslateX = (-1) * point.X + this.transform.TranslateX;
                double maxTranslateY = this.scrollViewer.ActualHeight - point.Y - height + this.transform.TranslateY;
                double minTranslateY = (-1) * point.Y + this.transform.TranslateY;
    
                this.transform.TranslateX = Math.Min(this.transform.TranslateX + e.Delta.Translation.X, maxTranslateX);
                this.transform.TranslateX = Math.Max(this.transform.TranslateX, minTranslateX);
                this.transform.TranslateY = Math.Min(this.transform.TranslateY + e.Delta.Translation.Y, maxTranslateY);
                this.transform.TranslateY = Math.Max(this.transform.TranslateY, minTranslateY);
    
                this.transform.ScaleX *= e.Delta.Scale;
                this.transform.ScaleY *= e.Delta.Scale;
    
            }

    Hope this helps 

    Aaron
    MSDN Community Support | Feedback to us
    Develop and promote your apps in Windows Store
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    2013年1月28日 12:00
    版主
  • Aaron, 非常感謝. 當imgGrid size小於scroll viewer的時候, 你的sample的確可以讓imgGrid限制在scroll viewer當中移動. 

    但是, 當imgGrid一開始就是佔滿scroll viewer的時候, 如果再將imgGrid放大, 讓其超出邊界, 那麼之後的拖拉移動就還是會讓imgGrid跑不見. 

    不知道應該如何修改, 能讓imgGrid的size在大於scroll viewer的情況下, 拖拉移動也可以讓imgGrid的邊界限制在範圍之內?

    例如: 一開始imgGrid的左上角邊界就貼齊scroll viewer左上角, 之後對imgGrid進行放大, 讓imgGrid左上角超出範圍之外, 然後再將imgGrid往右下方拖拉, 此時要怎麼改寫才能讓imgGrid往右下方拖拉的時候, imgGrid的左上角不會低於於scroll viewer的左上角?

    從應用來說, 就是可以讓一張圖片隨意放大縮小, 但是放大到超出螢幕之後, 為了讓使用者可以看到被遮掉的部分, 所以允許拖拉移動整張照片, 可是又不希望在拖拉移動的過程中讓使用者看到黑邊 (或是把照片整著移出螢幕). 

    再麻煩了. 

    2013年1月30日 3:08
  • Hi,

    抱歉,因为这个是现写的难免会有一些问题,你可以试试这个:

     private void imgGrid_ManipulationDelta_1(object sender, ManipulationDeltaRoutedEventArgs e)
            {
                var trans = this.imgGrid.TransformToVisual(this.scrollViewer);
                Point point = trans.TransformPoint(new Point(0, 0));
                Rect rect = trans.TransformBounds(new Rect(0, 0, 1, 1));
    
                double height = this.imgGrid.ActualHeight * rect.Height;
                double width = this.imgGrid.ActualWidth * rect.Width;
    
                double maxTranslateX = this.scrollViewer.ActualWidth - point.X - width + this.transform.TranslateX;
                double minTranslateX = (-1) * point.X + this.transform.TranslateX;
                double maxTranslateY = this.scrollViewer.ActualHeight - point.Y - height + this.transform.TranslateY;
                double minTranslateY = (-1) * point.Y + this.transform.TranslateY;
    
                if (maxTranslateX > 0 && minTranslateX < 0)
                {
                    this.transform.TranslateX = Math.Min(this.transform.TranslateX + e.Delta.Translation.X, maxTranslateX);
                    this.transform.TranslateX = Math.Max(this.transform.TranslateX, minTranslateX);
                }
                else
                {
                    this.transform.TranslateX = 0;
                }
                if (maxTranslateY > 0 && minTranslateY < 0)
                {
                    this.transform.TranslateY = Math.Min(this.transform.TranslateY + e.Delta.Translation.Y, maxTranslateY);
                    this.transform.TranslateY = Math.Max(this.transform.TranslateY, minTranslateY);
                }
                else
                {
                    this.transform.TranslateY = 0;
                }
    
                this.transform.ScaleX *= e.Delta.Scale;
                this.transform.ScaleY *= e.Delta.Scale;
    
            }

    这个代码思路是这样,因为我们没法确定imgGrid是否被缩放了,所以需要在ManipulationDelta中都需要检测一次,但是transform.TranslateX/TranslateY这个数值是相对于出发点的,这个通过两对Min、Max限制了X轴Y轴的移动范围,因此通过TransformToVisual得到的这个不断变化的数字计算这个由一个固定点的数据的时候,就需要加上原来的已经移动的位置,计算出准确的范围。


    Aaron
    MSDN Community Support | Feedback to us
    Develop and promote your apps in Windows Store
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    2013年1月31日 10:04
    版主
  • 如果只是做图片的缩放和移动功能,可以不用manipulation相关的东西.

    直接在ScrollViewer里放内容(Image,或者像你代码里的,先放置Grid),只要设置好ScrollViewer的Max(Min)ZoomFactor,ScrollViewer的Width(Height),内容的(Width,Height),就够了.

    2013年2月4日 0:43
  • Aaron, 感謝你的sample, 不過還是有個小問題, 就是: 當把圖片放大到超出螢幕(寬高都超出邊界), 那麼就沒辦法再移動了.  請問如果要做到寬高都超出邊界的情況下, 仍然可以移動, 但是移動時不會讓黑邊跑出來, 應該如何修改? 

    抱歉本來應該自行修改你的sample, 但是因為看不太懂, 所以不知道如何下手. 或是, 你可以詳細解說一下每一行的用途? 或是給相關的文件說明也可以. 

    2013年2月4日 8:27
  • Lichong, 我試過Scroll Viewer本身所提供的拖拉移動功能, 似乎僅限於手指觸控(gesture), 如果想要用滑鼠按住不放來做拖拉移動, 目前查到就是用manipulation. 或許你可以提供其他更快速簡便的方式來做到滑鼠按住進行拖拉移動? 
    2013年2月4日 8:36
  • Aaron, 另外多問一個問題, 請問當用手指觸控來進行縮放之後, 有辦法知道目前的imgGrid被縮放之後的大小嗎? 我檢查了 imgGrid的ActualWidth 和scrollviewer的ActualWidth, 發覺都沒有變化. 多謝. 
    2013年2月5日 11:26
  • 用鼠标操作,放大缩小是Ctrl+滚轮。拖动是拖动滚动条。

    你可以在Store里下载应用:Perfect365.里面的图像编辑功能就是这样子实现的。

    当然,如果你的需求是参考Photo应用的pan&zoom,那么肯定是得使用手势识别来做了。

    2013年2月5日 12:52
  • Hi,

    是的,因为我程序是这样写的所以没有办法移动,因为如果grid超出后面的ScrollViewer,限制移动边界就没有意义了,所以写成不能移动。

    这个代码思路是这样,因为我们没法确定imgGrid是否被缩放了,所以需要在ManipulationDelta中都需要检测一次,但是transform.TranslateX/TranslateY这个数值是相对于出发点的,这个通过两对Min、Max限制了X轴Y轴的移动范围,因此通过TransformToVisual得到的这个不断变化的数字计算这个由一个固定点的数据的时候,就需要加上原来的已经移动的位置,计算出准确的范围。

    这段话是解释这个程序的,如果你不太理解可以看这个图:

    至于你说的第二个问题,我也发现了,而且手势缩放还好可以通过e.scale来得到放大缩小系数,但是ctrl放大缩小则完全得不到系数,也得不到长宽这应该是元素没有重绘的原因,解决方案就是使用TranformToVisual()方法,文档:

    http://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.xaml.uielement.transformtovisual.aspx


    如上图所示,A,B两个矩形,a,b,c,d,e,f,6个线段。a,b的长度可以通过TransformToVisual和tranformPoint得到,具体见上面程序,因为TransformToVisual可以得到这个元素是否放大缩小,所以这个系数我们通过计算得到,使用TranformBounds一个长宽皆为1的正方形得到缩放系数,这个对鼠标手势缩放都可以用。但因为我们不知道这个元素什么时候放大缩小,所以每一次都需要重新检测得到系数计算width和height

    以A矩形中心为原点(0,0)向右移动和向下移动时,得到的e.Delta.translation.X和Y是正数,相反为负数,但是这个数值是以A矩阵就是一开始中心为起点,

    this.transform.TranslateX/Y控制横向纵向移动,因此这个数值就需要限定在c,d这两个长度上,但是你可以看到这两个数值跟长和宽有关,因此我们才需要通过系数计算实际的长和宽。比如得到d,通过ScrollViewer.width-a(point.x)-width(实际宽度)即可,但是point.x跟随移动而改变,但是d的数值只根据width的宽度改变,这个公式减去point.x的时候实际上也减去了横向移动的距离(上一次的距离),因此我们只需要加上这一次的距离就可以得到这个固定的d。

    上面是最大值的计算思路,最小值相似,向左移动则得到的e.Delta.translation.X是负数,因此乘上-1,相同的原因我们需要加回来这一次的移动距离,因此加上this.translate.X

    不过,最大值和最小值需要分别保持为正数和负数。因此我加上判断。最后乘上e.delta.scale的原因是针对手势,如果用手势放大缩小通过上面计算出的最大最小值以后,通过这个来放大缩小元素。

    你可以按照你自己需要改一下程序。如果还有不明白请告诉我。


    Aaron
    MSDN Community Support | Feedback to us
    Develop and promote your apps in Windows Store
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    2013年2月6日 9:34
    版主
  • Hi, Aaron, 感謝你的解說, 現在比較清楚大概是怎麼回事:

        Point point = trans.TransformPoint(new Point(0, 0)); --> 這個是imgGrid左上角的座標值
        double width = this.imgGrid.ActualWidth * rect.Width; --> 這個是imgGrid縮放之後的寬度

    有了這些, 就可以判斷目前的imgGrid是否在螢幕範圍之內, 並做出處理. 例如: 如果左上角的點進入螢幕範圍內, 就不允許平移: 

         if ((point.X + e.Delta.Translation.X) > 0)
               this.transform.TranslateX = 0;

    這樣是可以讓左上角一進入螢幕範圍就停止移動, 可是, 還是有可能會因為移動速度快, 結果進入了一段距離後才停止移動, 所以還是會出現黑邊 (左邊沒有貼齊螢幕邊緣). 這樣還蠻奇怪的, 因為上面的代碼應該就已經限制了這樣的可能性. 不知道是不是有疏忽遺漏的地方? 

    2013年2月19日 2:20
  • Hi,

    这个是避免不了的,原因是PointerMoved有一定的触发频率。虽然很快但是取消不了间隔,每次触发事件的时候才会判断。


    Aaron
    MSDN Community Support | Feedback to us
    Develop and promote your apps in Windows Store
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    2013年2月20日 4:43
    版主