TabControl گرافیکی و Toggle دو حالته در WPF
Loading...
TabControl

استفاده از نمونه آماده TabControl برخی از مشکلات مربوط به رفتار و ارتقاهای ممکن TabControl پیش فرض ویندوز را افزایش می دهد. در این مقاله ما سعی می کنیم که TabControl را هم از نظر ظاهری و هم از نظر رفتاری ارتقا دهیم. برای این کار از تکنیک Graphic-Elements Binding (اتصال عناصر گرافیکی) برای دست یابی به ظاهر Cardboard-folder-splitters و Toggle دو حالته استفاده خواهیم کرد.

نکات مهم:

این کد در ویژوال استودیو ۲۰۱۲ و با .net framework 4.5 نوشته شده است.

این کد از کتابخانه های زیر استفاده می کند:

Microsoft.Expression.Intractions

System.Windows.Interactivity

در ویژوال استودیو ۲۰۱۲، بخش هایی از XAML خطاهای زمان طراحی ایجاد می کنند (درحالی که ویژوال استودیو ۲۰۱۰ آن را نادیده می گیرد.)

کامپایلر زمان اجرا این XAML را می پذیرد و اپلیکیشن را به عنوان استثنا اجرا می کند.

برای کار در زمان طراحی و داشتن Designer-View، کد مشخص شده در زیر را کامنت نمایید.

...
<!--designtime bug - COMMENT for design-time view-->
<PolyBezierSegment >
	<PolyBezierSegment.Points>
		<MultiBinding Converter="
		{StaticResource HeaderContentHeightNumOfItemsItemIndex2HeaderLeftCurves}">
			<Binding ElementName="gridHeaderContent"
			Path="ActualHeight"/>
			<Binding RelativeSource="
			{RelativeSource AncestorType=TabControl}"
			Path="Items.Count"/>
			<Binding RelativeSource="
			{RelativeSource AncestorType=TabItem}"
			Path="(TabControl.AlternationIndex)"/>
		</MultiBinding>
	</PolyBezierSegment.Points>
</PolyBezierSegment>
<!--designtime bug - COMMENT for design-time view-->

...

TabControl

پیش زمینه:

زمانی که UI اپلیکیشن جدیدی که ما ایجاد کردیم، برای استفاده از TabControl فراخوانی می شود، چندین مشکل به چشم می خورد:

ظاهر آن ساده و شبیه پنجره های فرم ویندوز است.

عنوان TabItem، فقط بخشی از فضای قابل استفاده را اشغال می کند و حتی بدتر از آن، زمانی که فضا پر می شود سطر یا ستون جدیدی اضافه می کند که گیج کننده می شود.

زمانی که محتوای TabItem حجم بالایی دارد، رابط گرافیکی TabControl هنگ می کند و اصطلاحا فریز می شود.

نکته قبلی اشاره به حالت غیرعادی دارد که TabControl محتوای TabItem انتخاب شده را نمایش می دهد.

با هر بار انتخاب TabItem محتوای آن دوباره بارگذاری (Load) می شود.

در نگاه اول، شاید این رفتار بسیار مشکل ساز باشد، مفاله ها و کدهای بسیاری وجود دارد که هدف آنها جلوگیری از این مشکل می باشد.

منطق این کار بستگی به TabControl اپلیکیشن ما دارد، این TabControl ممکن است شامل کنترل های بسیاری باشد که در TabItemها قرار گرفته اند یا ممکن است TabControl فقط یکی از کنترل های بسیاری باشد که در کل برنامه استفاده شده است. ایجاد و حفظ همه کنترل ها در TabControl فشار زیادی به برنامه می آورد.

بخش کد:

قالب های آماده TabControl

ویژگی قابل توجه این TabControl، ظاهر آن است، TabItem های آن با بردارهای حاشیه شکل گرفته ای تعریف شده اند که طوری کشیده می شوند که از حداکثر فضای موجود استفاده کنند، که این در دو بخش از استایل TabControl انجام شده است:

قالب TabControl

قالب ItemContainerStyle مربوط به TabControl

هر یک از این قالب ها شامل یک گرید دو ستونه برای مقایسه است (ستون اول برای عنوان آیتم ها و دیگری برای محتوای آنها)

قالب TabControl با ستون سمت راست کار دارد که شامل:

Content –Presenter (نمایشگر محتوا) برای محتوای TabItem انتخاب شده است.

<ContentPresenter   x:Name="content"  Opacity="0" ContentSource="SelectedContent" 
ContentTemplate="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, 
Path=SelectedItem.ContentTemplate}">
	<i:Interaction.Triggers >
		<i:EventTrigger EventName="Loaded">
			<app:actSelectedContentDisplayHandler
			ContentTemplate ="{Binding RelativeSource=
		{RelativeSource AncestorType=TabControl},Path=SelectedItem.ContentTemplate}"
			LoadingTextBlock ="{Binding ElementName=tbLoading}"
			/>
		</i:EventTrigger>
	</i:Interaction.Triggers>
</ContentPresenter>

 

بردار حاشیه مشترک بین همه TabItem ها

<Path Grid.Column="1" Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{x:Static Member=app:TabListBoxConstants.StrokeThickness}"
Stretch="None"    Fill="White">
    <Path.Resources>
        <app:HeaderContentWidth2ContentTopLinePoint
        x:Key="HeaderContentWidth2ContentTopLinePoint"/>
        <app:HeaderContentWidth2ContentRTPoint1
        x:Key="HeaderContentWidth2ContentRTPoint1"/>
        <app:HeaderContentWidth2ContentRTPoint2
        x:Key="HeaderContentWidth2ContentRTPoint2"/>
        <app:HeaderContentWidthHeight2ContentRightLinePoint
        x:Key="HeaderContentWidthHeight2ContentRightLinePoint"/>
        <app:HeaderContentWidthHeight2ContentRBPoint1
        x:Key="HeaderContentWidthHeight2ContentRBPoint1"/>
        <app:HeaderContentWidthHeight2ContentRBPoint2
        x:Key="HeaderContentWidthHeight2ContentRBPoint2"/>
        <app:HeaderContentHeight2ContentBottomLinePointForContent
        x:Key="HeaderContentHeight2ContentBottomLinePointForContent"/>
    </Path.Resources>
    <Path.Data>
        <PathGeometry >
            <PathFigure IsClosed="False" >

                <PathFigure.StartPoint>
                    <Point X="{x:Static Member=app:TabListBoxConstants.CornerRadios}"
                    Y="{x:Static Member=app:TabListBoxConstants.HalfStrokeThickness}"/>
                </PathFigure.StartPoint>
                <LineSegment Point="{Binding ElementName=gridHeaderContent,
                Path=ActualWidth,Converter=
		{StaticResource HeaderContentWidth2ContentTopLinePoint}}"/>
                <QuadraticBezierSegment
                            Point1="{Binding ElementName=gridHeaderContent,
                            Path=ActualWidth,Converter={StaticResource
                            HeaderContentWidth2ContentRTPoint1}}"
                            Point2="{Binding ElementName=gridHeaderContent,
                            Path=ActualWidth,Converter={StaticResource
                            HeaderContentWidth2ContentRTPoint2}}"
                            />
                <LineSegment>
                    <LineSegment.Point >
                        <MultiBinding Converter="
                        {StaticResource HeaderContentWidthHeight2ContentRightLinePoint}">
                            <Binding ElementName="
                            gridHeaderContent" Path="ActualWidth"/>
                            <Binding ElementName="
                            gridHeaderContent" Path="ActualHeight"/>
                        </MultiBinding>
                    </LineSegment.Point>
                </LineSegment>

                <QuadraticBezierSegment >
                    <QuadraticBezierSegment.Point1>
                        <MultiBinding Converter="
                        {StaticResource HeaderContentWidthHeight2ContentRBPoint1}">
                            <Binding ElementName="
                            gridHeaderContent" Path="ActualWidth"/>
                            <Binding ElementName="
                            gridHeaderContent" Path="ActualHeight"/>
                        </MultiBinding>
                    </QuadraticBezierSegment.Point1>
                    <QuadraticBezierSegment.Point2>
                        <MultiBinding Converter="
                        {StaticResource HeaderContentWidthHeight2ContentRBPoint2}">
                            <Binding ElementName="
                            gridHeaderContent" Path="ActualWidth"/>
                            <Binding ElementName="
                            gridHeaderContent" Path="ActualHeight"/>
                        </MultiBinding>
                    </QuadraticBezierSegment.Point2>
                </QuadraticBezierSegment>
                <LineSegment Point="{Binding ElementName=gridHeaderContent,
                Path=ActualHeight,Converter={StaticResource
                HeaderContentHeight2ContentBottomLinePointForContent}}"></LineSegment>
            </PathFigure>
        </PathGeometry>
    </Path.Data>
</Path>

 

پیغام ” در حال بارگذاری…” (loading…)

<TextBlock x:Name="tbLoading"  Text="Loading..."
    FontSize ="18" HorizontalAlignment ="Center"
    VerticalAlignment="Center" Opacity="0"></TextBlock>

قالب ItemContainerStyle بیشتر با ستون سمت چپ (عنوان TabItem) کار می کند:

Content –Presenter (نمایشگر محتوا) برای محتوای TabItem انتخاب شده است.

<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"   
Content="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Header}"/>

شکل خاص بردار حاشیه برای TabItem:

<Path Grid.ColumnSpan="2"  x:Name="path"  
Stroke="{TemplateBinding BorderBrush}"   StrokeThickness="
{x:Static Member=app:TabListBoxConstants.StrokeThickness}" Stretch="None"
Fill="White"  >
    <Path.Data >
    <PathGeometry >
    <PathFigure  IsClosed="False"
    StartPoint="{Binding ElementName=gridHeaderContent,Path=ActualHeight,
    Converter={StaticResource HeaderContentHeight2ContentBottomLinePoint}}" >
    <QuadraticBezierSegment
    Point1="{Binding ElementName=gridHeaderContent,Path=ActualHeight,
    Converter={StaticResource HeaderContentHeight2ContentLBPoint1}}"
    >
    <QuadraticBezierSegment.Point2>
    <MultiBinding Converter="{StaticResource 
    HeaderContentHeightNumOfItemsItemIndex2HeaderContentLBPoint2   }">
    <Binding ElementName="gridHeaderContent" 
    Path="ActualHeight"/>
    <Binding RelativeSource="{RelativeSource AncestorType=TabControl}" 
    Path="Items.Count"/>
    <Binding RelativeSource="{RelativeSource AncestorType=TabItem}" 
    Path="(TabControl.AlternationIndex)"/>
</MultiBinding>

</QuadraticBezierSegment.Point2>
</QuadraticBezierSegment>

...

 

شکل خاص بردارحاشیه پس زمینه برای TabItem:

<Path x:Name="pathdark" Grid.ColumnSpan="2" 
Visibility="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=IsSelected,Converter={StaticResource IsSelected2NotVisibility}}"  
Stroke="Gray"   StrokeThickness="
{x:Static Member=app:TabListBoxConstants.StrokeThickness}" 
Stretch="None"  Margin="0" SnapsToDevicePixels="True"
Fill="WhiteSmoke"  >
    <Path.Data >
    <PathGeometry >

    <PathFigure  IsClosed="False"
    StartPoint="{Binding ElementName=gridHeaderContent,
    Path=ActualHeight,Converter={StaticResource HeaderContentHeight2ContentBottomLinePoint}}" >

    <QuadraticBezierSegment
    Point1="{Binding ElementName=gridHeaderContent,
    Path=ActualHeight,Converter={StaticResource HeaderContentHeight2ContentLBPoint1}}"
    >
    <QuadraticBezierSegment.Point2>
    <MultiBinding Converter="
    {StaticResource HeaderContentHeightNumOfItemsItemIndex2HeaderContentLBPoint2   }">
    <Binding ElementName="gridHeaderContent" 
    Path="ActualHeight"/>
    <Binding RelativeSource="{RelativeSource AncestorType=TabControl}" 
    Path="Items.Count"/>
<Binding RelativeSource="{RelativeSource AncestorType=TabItem}" 
Path="(TabControl.AlternationIndex)"/>
</MultiBinding>

</QuadraticBezierSegment.Point2>
</QuadraticBezierSegment>

...

 

TabItem ها به صورت ظاهری با یک بردار silhouette (تصویر یا سایه سیاهی که در پس زمینه سفید نمایش داده شود) تعریف شده اند. خط بردار با یک المنت path ایجاد شده است که شامل تعدادی بخش هندسی مختلف است. پارامترهای هر بخش به متغیرهای مختلفی مانند عرض/ارتفاع فضای موجود، عرض ثابت عنوان، تعداد آیتم ها، اندیس هر آیتم و… متصل (Bind) می شوند. پیدا کردن مقدار پارامترهای بخش های اصلی با استفاده از مجموعه ای از ValueConvertor های موردی (Case-based) انجام می شود.

برای مثال:

...
<LineSegment >
      <LineSegment.Point>
          <MultiBinding Converter="
          {StaticResource HeaderContentHeightNumOfItemsItemIndex2ContentLeftBottomLinePoint}">
              <Binding ElementName="
              gridHeaderContent" Path="ActualHeight"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabControl}"
              Path="Items.Count"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabItem}"
              Path="(TabControl.AlternationIndex)"/>
          </MultiBinding>
      </LineSegment.Point>
  </LineSegment>
  <QuadraticBezierSegment>
      <QuadraticBezierSegment.Point1 >
          <MultiBinding Converter="
          {StaticResource HeaderContentHeightNumOfItemsItemIndex2HeaderRBPoint1}">
              <Binding ElementName="gridHeaderContent"
              Path="ActualHeight"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabControl}"
              Path="Items.Count"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabItem}"
              Path="(TabControl.AlternationIndex)"/>
          </MultiBinding>
      </QuadraticBezierSegment.Point1>
      <QuadraticBezierSegment.Point2 >
          <MultiBinding Converter="
          {StaticResource HeaderContentHeightNumOfItemsItemIndex2HeaderRBPoint2}">
              <Binding ElementName="gridHeaderContent"
              Path="ActualHeight"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabControl}"
              Path="Items.Count"/>
              <Binding RelativeSource="
              {RelativeSource AncestorType=TabItem}"
              Path="(TabControl.AlternationIndex)"/>
          </MultiBinding>
      </QuadraticBezierSegment.Point2>
  </QuadraticBezierSegment>
...

 

توجه داشته باشید که این قالب درواقع جایگزین درخت XAML در حالتی است که محتوای TabItem در شاخه های مختلفی از درخت XAML قرار گرفته باشد.

ریسپانسیو بودن UI زمانی که TabItem حجم بالایی دارد:

همانطور که گفتیم، در برخی موارد رابط کاربری TabControl هنگ (Freeze) می کند، این مشکل را با استفاده از TriggerAction موجود در فضای نام System.Windows.Interactivity به نام actSelectedContentDisplayHandler برطرف کردیم.

ایده اصلی این کلاس انجام کارهای زیر برای هر محتوای جدیدی که انتخاب شده و نیاز به لود شدن دارد، می باشد:

پنهان کردن محتوای قبلی انتخاب شده

نمایش پیغام “Loading…”

اجرا کردن NOT UI-Thread مربوط به بارگذاری آسنکرون در حالی که رابط کاربری اپلیکیشن را فعال نگه می دارد

اجرا کردن UI-Thread مربوط به بارگذاری زمانی که UI-Thread کمترین زمان لازم را می گیرد.

زمانی که عناصر محتوا بارگذاری شدند، محتوای TabItem را نمایش داده و پیام loading… را پنهان می کند.

<ContentPresenter   x:Name="content"  Opacity="0" ContentSource="SelectedContent" 
ContentTemplate="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, 
Path=SelectedItem.ContentTemplate}">
    <i:Interaction.Triggers >
        <i:EventTrigger EventName="Loaded">
            <app:actSelectedContentDisplayHandler
                ContentTemplate ="{Binding RelativeSource={RelativeSource AncestorType=TabControl},
		Path=SelectedItem.ContentTemplate}"
                LoadingTextBlock ="{Binding ElementName=tbLoading}"
                />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ContentPresenter>

private async static void ContentTemplateChanged
    (DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
    ContentPresenter ContPres =
    ((actSelectedContentDisplayHandler)obj).AssociatedObject;// obj.GetValue
    		//(contentPresenterProperty) as ContentPresenter;
    if (ContPres == null)
    {
        return;
    }

    TextBlock tbLoading = obj.GetValue(LoadingTextBlockProperty) as TextBlock;

    ContPres.Opacity = 0;
    tbLoading.Opacity = 1;

    CancellationTokenSource cts = new CancellationTokenSource();

    Task t = Task.Factory.StartNew(() =>
    {
        AutoResetEvent are = new AutoResetEvent(false);
        Application.Current.Dispatcher.BeginInvoke
        (DispatcherPriority.ApplicationIdle, new Action(() =>
        {
            DataTemplate ContTemp =
            obj.GetValue(ContentTemplateProperty) as DataTemplate;
            ContPres.ContentTemplate = ContTemp;

            ContPres.SetValue(areProperty, are);
        }));

        are.WaitOne(1000);
    }, cts.Token);
    ContPres.SetValue(ActiveTaskProperty, t);
    await t;
    ContPres.SetValue(ActiveTaskProperty, null);
    ContPres.Opacity = 1;
    tbLoading.Opacity = 0;
}

void AssociatedObject_LayoutUpdated(object sender, EventArgs e)
{
    if (this.AssociatedObject.GetValue
    (actSelectedContentDisplayHandler.ActiveTaskProperty) != null) // cts!=null)
    			// behaviorTabListBox.t !=null)
    {
        if (VisualTreeHelper.GetChildrenCount(this.AssociatedObject) > 0)
        {
            (this.AssociatedObject.GetValue
            (actSelectedContentDisplayHandler.areProperty) as AutoResetEvent).Set();

        }
    }
}

 

Toggle دو حالته:

یکی از مشکلات مربوط به رابط کاربری، استفاده از ToggleButton یا Button برای تغییر و جابه جا شدن بین دو حالت نمایش است.

در بسیاری از موارد، رابطه بین کنترل و فضایی که قرار است تغییر کند واضح نیست، به طور مثال متن روی Button گیج کننده است، یا حالتی را نمایش می دهد که حالت نمایش فعلی نیست و در بعضی موارد دیگر نام حالت نمایشی که در حال حاضر انتخاب شده را نمایش می دهد.

در این بخش، راه حل UI جایگزینی را برای این مشکل ارائه می دهیم. در حالی که همان زبان طراحی رابط کاربری را حفظ می کنیم، اما این ویژگی را برای کاربر بسیار قابل فهم خواهیم کرد.

نحوه ایجاد این ویژگی بسیار شبیه به ایجاد TabControl است و تنها تفاوت آن حالت انیمیشنی جا به جایی بین دو حالت می باشد.

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="ShowStates">
        <VisualState x:Name="ShowLeftTab">
            <Storyboard >
                <DoubleAnimationUsingKeyFrames Storyboard.
                TargetName="LeftTab" Storyboard.
                TargetProperty="Opacity">
                    <EasingDoubleKeyFrame KeyTime="0:0:0.2"
                    Value="1"></EasingDoubleKeyFrame>
                </DoubleAnimationUsingKeyFrames>
                <Int32AnimationUsingKeyFrames Storyboard.TargetName="
                LeftTab" Storyboard.TargetProperty="(Panel.ZIndex)">
                    <DiscreteInt32KeyFrame KeyTime="0:0:0"
                    Value="4"></DiscreteInt32KeyFrame>
                </Int32AnimationUsingKeyFrames>
                <DoubleAnimation Storyboard.TargetName="LeftContent"
                Storyboard.TargetProperty="Opacity" To="1">
                </DoubleAnimation>

                <Int32AnimationUsingKeyFrames Storyboard.TargetName="
                RightContent" Storyboard.TargetProperty="(Panel.ZIndex)">
                    <DiscreteInt32KeyFrame KeyTime="0:0:0.2"
                    Value="1"></DiscreteInt32KeyFrame>
                </Int32AnimationUsingKeyFrames>
                <DoubleAnimation Storyboard.TargetName="RightContent"
                Storyboard.TargetProperty="Opacity" To="0">
                </DoubleAnimation>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="ShowRightTab">
            <Storyboard >
                <DoubleAnimationUsingKeyFrames Storyboard.TargetName="
                LeftTab" Storyboard.TargetProperty="Opacity">
                    <EasingDoubleKeyFrame KeyTime="0:0:0.2"
                    Value="0"></EasingDoubleKeyFrame>
                </DoubleAnimationUsingKeyFrames>
                <Int32AnimationUsingKeyFrames Storyboard.TargetName="
                LeftTab" Storyboard.TargetProperty="(Panel.ZIndex)">
                    <DiscreteInt32KeyFrame KeyTime="0:0:0.2"
                    Value="2"></DiscreteInt32KeyFrame>
                </Int32AnimationUsingKeyFrames>
                <DoubleAnimation Storyboard.TargetName="LeftContent"
                Storyboard.TargetProperty="Opacity" To="0">
                </DoubleAnimation>

                <Int32AnimationUsingKeyFrames Storyboard.TargetName="
                RightContent" Storyboard.TargetProperty="(Panel.ZIndex)">
                    <DiscreteInt32KeyFrame KeyTime="0:0:0.2"
                    Value="3"></DiscreteInt32KeyFrame>
                </Int32AnimationUsingKeyFrames>
                <DoubleAnimation Storyboard.TargetName="RightContent"
                Storyboard.TargetProperty="Opacity" To="1">
                </DoubleAnimation>
                <DoubleAnimation Storyboard.TargetName="RightTabFront"
                Storyboard.TargetProperty="Opacity" To="0"
                Duration="0:0:0.1"></DoubleAnimation>
            </Storyboard>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

 



avatar داریوش فرخی

داریوش فرخی هستم از سال 92 شروع به یادگیری برنامه نویسی و از سال 93 در بخش برنامه نویسی و تولید محتوای سایت mspsoft.com مشغول هستم. فعالیتم نیز بیشتر در زمینه های برنامه نویسی با سی شارپ و asp.net بوده است. اوقات فراغتم را هم غالبا با تماشای فیلم یا بازی های کامپیوتری پر میکنم .

آخرین مطالب و تخفیفات در کانال تلگرام :) کانال تلگرام ام اس پی سافت
ديدگاه خود را ارسال کنيد


محبوب ترين ويدئو هاي انلاين
دوره برنامه نویسی فروشگاه اینترنتی
  • تعداد اعضا 80k
  • قيمت دوره۱۰۰,۰۰۰ تومان
  • امتيازدهي
    1 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 5( 5٫00 از 1 رای )
    Loading...
دوره آموزشی سیستم ثبت سفارش آنلاین
  • تعداد اعضا 80k
  • قيمت دوره۵۰,۰۰۰ تومان
  • امتيازدهي
    1 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 5( 5٫00 از 1 رای )
    Loading...
دوره طراحی سیستم مدیریت مشتریان
  • تعداد اعضا 80k
  • قيمت دوره۵۰,۰۰۰ تومان
  • امتيازدهي
    1 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 51 vote, average: 5٫00 out of 5( 5٫00 از 1 رای )
    Loading...