`
RednaxelaFX
  • 浏览: 3014752 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

在Canvas上根据变量改变Shape的位置?

    博客分类:
  • WPF
阅读更多
昨晚有朋友问:
引用
Hi,帮我讲解一下WPF怎样在Canvas或者Grid上根据变量改变Shape的位置和形状吧~

没太理解问题在哪里,不过看样子是数据绑定方面不熟悉?
那就写个用到Canvas和数据绑定的例子吧。在VS2008里新建一个WPF应用,然后把下面的Window1.xaml和Window1.xaml.cs替换进去就行。

做出来的是像这样的一个界面(是很丑啦 T T)

把Window里的根容器Grid分成上下两行:上半部分放置用于控制和显示坐标的控件;下半部分放置一个Canvas,里面放一个Rectangle。在TextBox里输入数字或者滑动ScrollBar都能够改变Rectangle的位置。

也就是随便在VS2008的WPF Designer里拖拖控件把界面拉出来:
Window1.xaml
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Class="TestWpfCanvasShapeDataBinding.Window1"
    xmlns:Custom="http://schemas.microsoft.com/winfx/2006/xaml/composite-font"
    x:Name="mainWindow"
    DataContext="{Binding ElementName=mainWindow}"
    Title="Test Data Binding" Height="480" Width="230" >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBox Name="txtX" Margin="12,10,0,0" Height="23" Width="95"
                 VerticalAlignment="Top" HorizontalAlignment="Left"
                 Text="{Binding Path=RectX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Label Name="lblX" Margin="12,40,0,0" Height="23" Width="95"
               VerticalAlignment="Top" HorizontalAlignment="Left"
               Content="{Binding Path=RectX}" />
        <Button Name="btnX" Margin="0,10,5,0" Height="23" Width="81"
                VerticalAlignment="Top" HorizontalAlignment="Right"
                Click="button1_Click" >
                Check X Value
        </Button>
        <ScrollBar Name="scbX" Margin="12,70,5,0" Height="20" Width="181"
                   VerticalAlignment="Top" Orientation="Horizontal"
                   Maximum="200" Value="{Binding Path=RectX, Mode=TwoWay}" />
        
        <TextBox Name="txtY" Margin="12,120,0,0" Height="23" Width="95"
                 VerticalAlignment="Top" HorizontalAlignment="Left"
                 Text="{Binding Path=RectY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Label Name="lblY" Margin="12,150,0,0" Height="23" Width="95"
               VerticalAlignment="Top" HorizontalAlignment="Left"
               Content="{Binding Path=RectY}" />
        <Button Name="btnY" Margin="0,120,5,0" Height="23" Width="81"
                VerticalAlignment="Top" HorizontalAlignment="Right"
                Click="button2_Click" >
                Check Y Value
        </Button>    
        <ScrollBar Name="scbY" Margin="12,180,5,0" Height="20" Width="181"
                   VerticalAlignment="Top" Orientation="Horizontal"
                   Value="{Binding Path=RectY, Mode=TwoWay}" Maximum="200" />
        
        <Canvas Margin="0,0,0,0" Grid.Row="1" >
            <Canvas.Background>
              <Custom:LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5" >
                      <Custom:GradientStop Color="#FF337496" Offset="0" />
                      <Custom:GradientStop Color="#FF94E2EC" Offset="1" />
              </Custom:LinearGradientBrush>
            </Canvas.Background>
            <Rectangle Height="20" Width="20" Stroke="#FF301A87" Fill="#FF8169E6"
                       Canvas.Left="{Binding Path=RectX}"
                       Canvas.Top="{Binding Path=RectY}" />
        </Canvas>
    </Grid>
</Window>


那么来看看这个界面涉及到哪些数据绑定。

数据源方面,Window1里有两个int类型的属性,RectX和RectY,分别用于指定位于Canvas内的Rectangle的X和Y坐标。
更新:Window1的DataContext原本在代码里设置为了this,现在改为在XAML里直接设置。

接下来看看绑定目标方面。首先是TextBox。两个TextBox分别与RectX和RectY做了双向绑定,也就是说当RecX或RectY有了更新,则对应的TextBox会马上反应更新,而用户在TextBox中输入数字的时候RectX或RectY也会得到相应的更新。使用标记扩展(markup extension)语法来指定绑定:
<TextBox Name="txtX"
  Text="{Binding Path=RectX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

这里要注意的是,如果不显式指定TextBox的Text数据绑定中的UpdateSourceTrigger,则默认为LooseFocus,那么要等到TextBox失去焦点后才会发生TextBox->source的更新;而这里我们想要的是文本框里的文本发生改变时就马上更新。
绑定方向一共有4种:OneWay、TwoWay、OneTime和OneWayToSource。
OneWay就是目标根据数据源变化;
TwoWay就是目标和数据源相互都能更新;
OneTime就是目标只在初始化的时候读取一次数据,以后就不再跟随数据源而变化;
OneWayToSource是OneWay的反向,在目标的数据更新的时候也更新到数据源上。这主要是为了让没有DependencyProperty的属性能被有DependencyProperty的属性更新。
如果不使用标记扩展,也可以用传统的XML语法来指定数据绑定,像这样:
<TextBox Name="txtX" >
    <TextBox.Text>
        <Binding Path="RectX" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" />
    </TextBox.Text>
</TextBox>


然后是两个Label。它们只是对RectX和RectY做了单向绑定,也就是当RectX或RectY有了更新,则对应的Label会马上反应更新。
<Label Name="lblX"
  Content="{Binding Path=RectX}" />

由于Label的Content默认就是OneWay的,这里就没有显式指定。

接着,两个ScrollBar。跟TextBox相似,也是做了双向绑定。不过ScrollBar的Value不用显式指定UpdateSourceTrigger也行。
<ScrollBar Name="scbX"
  Value="{Binding Path=RectX, Mode=TwoWay}" />


最后是Canvas里的Rectangle。与Label类似,对RectX和RectY做了单向绑定,分别绑定到Canvas.Left和Canvas.Top这两个附加属性上。
<Rectangle
  Canvas.Left="{Binding Path=RectX}"
  Canvas.Top="{Binding Path=RectY}"/>


OK,到这里为止都是在XAML里设置数据绑定的目标。那数据源的一侧要如何实现呢?关键问题是,当数据源的值发生了变化,应该如何通知数据绑定的目标?

=====================================================================

(不是特别推荐的方法)通过实现INotifyPropertyChanged接口来实现数据源

WPF能理解INotifyPropertyChanged接口,通过其PropertyChanged事件来得到数据源更新的通知。

using System.ComponentModel;
using System.Windows;

namespace TestWpfCanvasShapeDataBinding {
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window, INotifyPropertyChanged {

        private int _rectX;
        private int _rectY;

        public int RectX {
            get { return _rectX; }
            set {
                _rectX = value;
                OnPropertyChanged( "RectX" );
            }
        }

        public int RectY {
            get { return _rectY; }
            set {
                _rectY = value;
                OnPropertyChanged( "RectY" );
            }
        }

        public Window1( ) {
            InitializeComponent( );
            //this.DataContext = this;
        }

        private void button1_Click( object sender, RoutedEventArgs e ) {
            MessageBox.Show( this.RectX.ToString( ) );
        }

        private void button2_Click( object sender, RoutedEventArgs e ) {
            MessageBox.Show( this.RectY.ToString( ) );
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged( string propertyName ) {
            var handler = PropertyChanged;
            if ( null != handler ) {
                handler( this, new PropertyChangedEventArgs( propertyName ) );
            }
        }

        #endregion
    }
}


实现要点是:
1、实现INotifyPropertyChanged接口,并声明其成员PropertyChanged事件;
2、定义一个OnPropertyChanged()方法来发送上述事件;不一定要叫OnPropertyChanged,这只是习惯;
3、在需要通知更新的属性的setter里调用OnPropertyChanged()。

这种做法在WinForms里应该很常见,因为WinForms的数据绑定支持实在算不上好。WPF里有更方便更强大的支持,也就是DependencyProperty。

=====================================================================

通过DependencyProperty来实现数据源

DependencyProperty的内部实现机制描述起来觉得好复杂。我也没理解透彻。所以这里就不多说了,详细还是找本WPF的书来看吧。
自己需要写的代码方面则很简单,如下:
Window1.xaml.cs
using System.Windows;

namespace TestWpfCanvasShapeDataBinding {
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window {
        public static readonly DependencyProperty RectXProperty;
        public static readonly DependencyProperty RectYProperty;

        public int RectX {
            get { return ( int ) GetValue( RectXProperty ); }
            set { SetValue( RectXProperty, value ); }
        }

        public int RectY {
            get { return ( int ) GetValue( RectYProperty ); }
            set { SetValue( RectYProperty, value ); }
        }

        static Window1( ) {
            RectXProperty = DependencyProperty.Register( "RectX", typeof( int ), typeof( Window1 ) );
            RectYProperty = DependencyProperty.Register( "RectY", typeof( int ), typeof( Window1 ) );
        }

        public Window1( ) {
            InitializeComponent( );
        }

        private void button1_Click( object sender, RoutedEventArgs e ) {
            MessageBox.Show( this.RectX.ToString( ) );
        }

        private void button2_Click( object sender, RoutedEventArgs e ) {
            MessageBox.Show( this.RectY.ToString( ) );
        }
    }
}


实现要点是:
前提:数据源继承FrameworkElement。
1、为需要数据绑定的属性声明静态只读的DependencyProperty域;名字按习惯一般是要绑定的属性名+Property。例如Foo属性的DependencyProperty就叫FooProperty;
2、在静态构造器里通过DependencyProperty上的几个静态工厂方法(Register、RegisterAttached、RegisterAttachedReadOnly、RegisterReadOnly等)来初始化这些DependencyProperty域;
3、在需要做数据绑定的属性的setter里调用继承自DependencyObject类的SetValue()方法,在getter里调用GetValue()方法。WPF的UIElement本身就继承自DependencyObject,所以在它的子类里都可以使用SetValue()和GetValue()。
然后基本的DependencyProperty就设置好可以使用了。需要更精确的配置的话,还可以通过FrameworkPropertyMetadata之类的数据来指定默认的绑定方向、默认值等一系列属性。

嘛,基本上就这样吧~懒得复制粘贴的话直接用附件里的Solution也行。
顺便一提,那两个按钮我只是想测试一下数据源是否确实被更新了而已。实际上没啥用,可以忽略……
分享到:
评论
4 楼 RednaxelaFX 2009-01-01  
cajon 写道
过不久,项目中就要使用WPF了,我也要开始学习了。大家一起研究啦。

之前我在写那个用来查看.NET的类型层次的小工具的时候本来是想用WPF而不是WinForms来写的。但那个程序要用到Tree,而我对Model-View-ViewModel模式的熟悉程度太差了,没试出好的写法,所以还是暂时放下了,换回用相对熟悉些WinForms来写。

昨晚跟那朋友联系了,结果问题是:
引用
谢了哈,问题解决了。还记得上次问你的random movement algorithm吗,就是想做这种“动画”。我用DispatcherTimer搞定了

泪目,没“猜”对问题 T T

对了,祝大家新年快乐~
3 楼 cajon 2009-01-01  
RednaxelaFX 写道

结论是我还不会WPF啦。充其量也就只能做点像这样无聊的sample。Colin大有空多支几招来~~呵呵 &lt;(_ _)>

呵呵,WPF倒是关注的很早,但是,后来项目一忙也就忘得差不多了。你说了那么多概念,我还真是一个都不熟悉。昨天看到你在写,一时兴起,就写了一点。其中还有一些错误。比如:把AttachedProperty写成了AttachmentProperty。
过不久,项目中就要使用WPF了,我也要开始学习了。大家一起研究啦。
2 楼 RednaxelaFX 2008-12-31  
cajon 写道
我以为这个人问你这个问题的原因是因为他在Rectangle上找不到Left和Top属性的原因吧。

嗯,有道理,多谢Colin老大~这个细节我没想到,确实值得说明。昨天他问了问题就下线了,我也不知道具体是哪里有问题……
属性里Canvas.Left和Canvas.Top等附加属性,是attached properties没错。这种DependencyProperty通过DependencyProperty.RegisterAttached()来注册,使用的时候两种写法是等价的:
Canvas.SetLeft(rect, x);

rect.SetValue(Canvas.LeftProperty, x);

WPF我也是初学啦,整体上都还没用顺手。布局应该遵循怎样的guideline才比较整洁也没底;M-V-VM模式现在还迷糊着,如何通过M-V-VM模式来使用Tree也还没弄清楚;Adorner也是明明很重要但还不了解……T T
结论是我还不会WPF啦。充其量也就只能做点像这样无聊的sample。Colin大有空多支几招来~~呵呵 <(_ _)>
1 楼 cajon 2008-12-31  
哈哈,没想到FX对WPF也有研究。

我以为这个人问你这个问题的原因是因为他在Rectangle上找不到Left和Top属性的原因吧。我也来解释两句。

WPF中,认为一个对象不应该关心它的位置,位置是由该对象的容器来决定的。所以,在Rectangle上找不到Location的信息。这一点和WinForm不一样。

而在XAML中看到的Left和Top属性实际上是一个附加属性(好像英文是:AttachmentProperty).在XAML中需要写成Canvas.Left和Canvas.Top。这样的写法实际上就是在使用Canvas.SetLeft(rect, x)和Canvas.SetTop(rect. y)两个静态方法。这两个静态方法会将属性的值存储到PropertyStorage中。

之所以设计成这样,就是我前面所说的,其实Rectangle并不关心它的位置,而Canvas需要通过这两个属性决定Rectangle的位置,因此,这两个方法定义在Canvase上面。

同样的,对于Grid中的内容,就要通过Grid.SetRow Grid.SetCol和Grid.SetRowSpan等着几个方法了。

相关推荐

Global site tag (gtag.js) - Google Analytics