Bài 1: Ứng dụng các loại layout container để tạo User Interface

Trong các bài trước, chúng ta đã tìm hiểu về một số loại layout container sử dụng trong công nghệ WPF. Nếu chưa biết về các loại layout này, tôi khuyên bạn nên đọc các bài viết giới thiệu một số loại layout container cơ bản trước, đó là WrapPanel, DockPanelStackPanel. Trong bài viết này, chúng ta sẽ tìm hiểu thêm về một loại layout container rất mạnh – grid.

Trước hết, bạn nên đọc qua bài Hướng dẫn viết game Xếp gạch với C/C++ của anh tauit_dnmd. Ở đây tôi sẽ chỉ trình bày trò chơi với giao diện rất đơn sơ, không background, không sound.

Đầu tiên, chạy chương trình Visual Studio (hoặc Visual C#). Ở đây tôi dùng bản Visual Studio 2010 Ultimate. Ở khung bên trái – installed templates, chon Visual C# -> Windows. Tiếp theo, chọn WPF Application trong khung ở giữa. Đặt tên cho project và xác định vị trí lưu source files. Nhấn OK.

Ban đầu, Visual Studio sẽ tạo cho bạn 2 file: MainWindow.xaml – đây là phần tạo giao diện cho ứng dụng của bạn, và file MainWindow.xaml.cs – file này sẽ chứa phần code behind. Trong bài viết này chúng ta sẽ chỉ tạo ra giao diện cho chương trình, do đó chúng ta sẽ chỉ thao tác với file MainWindow.xaml mà thôi. Trước hết chúng ta sửa lại một vài thông tin về chương trình cái đã. Mà đã biết gì ngoài cái tên chương trình mà ta định viết đâu nhỉ, thôi kệ, sửa cái này trước cũng được ^^. Tìm tới Title attribute trong thẻ Window, sửa giá trị của thuộc tính này thành Simple Tetris (hoặc bất kì cái tên nào mà bạn muốn).

<Window x:Class="Tetris2.MainWindow"
        Title="Simple Tetris" ...
</Window>

Tiếp theo, ta thay thế layout container – Grid ban đầu mà WPF tạo ra cho chương trình của chúng ta bằng StackPanel. Như ở hình đầu tiên mà các bạn thấy, chúng ta phân chia bố cục theo hàng ngang, do đó chúng ta set thuộc tính Orientation thành Horizontal. Thêm một chi tiết nữa là background. Ở đây tôi chọn một cái màu gì gì đấy mà tôi cũng không biết ^^!, các bạn có thể chọn theo ý mình hoặc để trắng cũng được.

<StackPanel Background="AntiqueWhite" Orientation="Horizontal">
    …
</StackPanel>

Vậy là xong outer container. Bây giờ tiến hành tạo từng thành phần bên trong theo thứ tự từ trái sang phải. Trước hết sẽ là khung hướng dẫn cách chơi bên trái. Trong khung này sử dụng 4 textblock và được đặt trong một stack panel khác. Giá trị mặc định của thuộc tính Orientation trong StackPanel là Vertical cho nên chúng ta không cần phải set lại nữa. Dưới đây là phần mã xaml.

        <!-- Left side-->
        <StackPanel HorizontalAlignment="Left">
            <TextBlock  Margin="10,200,0,10" FontSize="14">
                Up arrow: Rotate
            </TextBlock>
            <TextBlock  Margin="10,0,0,10" FontSize="14">
                Left arrow: To the left
            </TextBlock>
            <TextBlock  Margin="10,0,30,10" FontSize="14">
                Right arrow: To the right
            </TextBlock>
            <TextBlock  Margin="10,0,0,10" FontSize="14">
                Down arrow: Get down
            </TextBlock>
        </StackPanel>

Tới phần quan trọng nhất, khung ở giữa chính là nơi chúng ta chơi game đấy các bạn ạ ^^. Trong phần này, chúng ta sẽ sử dụng một loại layout container rất mạnh mà tôi đã nhắc tới lúc đầu – Grid.

Giới thiệu về grid đã nhé. Bạn có thể tưởng tượng grid cũng như một ma trận 2 chiều vậy. Nó có thể có số hàng và số cột theo ý muốn của chúng ta, chỉ khác một điều là các hàng, cột mặc định sẽ không được hiển thị, trừ khi chúng ta set thuộc tính ShowGridLines thành true (hoặc trong lúc design ta cũng có thể thấy được các hàng và các cột này). Mỗi ô trong grid có thể chứa nhiều hơn một element và tất nhiên các element này cũng có thể là một loại layout container khác. Để thêm hàng, cột cho grid, chúng ta sử dụng 2 sub tag là <Grid.RowDefinitions> và <Grid.ColumnDefinitions>. Ví dụ tôi muốn tạo ra 2 hàng và 3 cột trong grid, đoạn mã XAML của tôi sẽ là:

<Grid.RowDefinitions>
     <RowDefinition></RowDefinition>
     <RowDefinition></RowDefinition>
</Grid.RowDefinitions>

<Grid.ColumnDefinitions>
     <ColumnDefinition></ColumnDefinition>
     <ColumnDefinition></ColumnDefinition>
     <ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>

Trong ví dụ trên, chúng ta không cung cấp thêm bất kì thông tin nào trong thẻ RowDefinition và ColumnDefinition, do đó Grid sẽ chia đều kích thước của mỗi ô tùy theo kích thước của cửa sổ ngoài cùng. Để đặt các thành phần vào các ô trong Grid, chúng ta phải sử dụng attached property – Row và Column. Ví dụ, chúng ta muốn đặt các button vào các ô trong Grid mà chúng ta tạo ra lúc trước, chúng ta sẽ làm như sau:

<Grid>
    ...
<Button Grid.Row="0" Grid.Column="0">Top Left</Button>
       <Button Grid.Row="0" Grid.Column="1">Middle Left</Button>
       <Button Grid.Row="1" Grid.Column="2">Bottom Right</Button>
       <Button Grid.Row="1" Grid.Column="1">Bottom Middle</Button>
    ...
</Grid>

Kết quả của đoạn mã trên:

Tiếp theo, chúng ta sẽ nói về phần kích thước của các hàng/cột trong Grid. Grid hỗ trợ 3 kiểu xác định kích thước như sau:

  • Absolute sizes. Bạn sẽ chọn kích thước cho hàng (cột) bằng cách sử dụng đơn vị đo lường, tính toán của WPF – Device-independent units.
  • Automatic sizes. Với cách này, mỗi hàng hoặc cột sẽ có kích thước vừa đủ để chứa các element đặt bên trong nó, không nhiều hơn và không ít hơn.
  • Proportional sizes. Khoảng cách được chia theo từng nhóm hàng hoặc cột. Với trường hợp này, các hàng, cột sẽ nhận được kích thước tối đa mà nó có thể có.

Để thay đổi kích thước của hàng, chúng ta sử dụng property Height, đối với cột là property Width. Ví dụ, về các trường hợp xác định kích thước:

<ColumnDefinition Width="100"></ColumnDefinition>

<!-- Sử dụng absolute sizes--> <ColumnDefinition Width="Auto"></ColumnDefinition>

<!-- Sử dụng Automatic sizes--> <RowDefinition Height="*"></RowDefinition>

<!-- Sử dụng Proportional sizes-->

Nếu bạn muốn các khoảng cách trong proportional size có tỉ lệ khác nhau, bạn chỉ cần thêm tỉ lệ phía trước dấu *:

<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="2*"></RowDefinition>

Cuối cùng, chúng ta sẽ nói đến việc trải dài kích thước element giữa các hàng, cột. Nếu bạn muốn một element đã được đặt vào một cell nào đó trong grid nhưng được kéo dãn và đè lên các cell khác trong cùng hàng hoặc cột, bạn sử dụng thuộc tính RowSpan và ColumnSpan. Ví dụ, một button sẽ sử dụng phần không gian của ô thứ nhất và ô thứ 2 trong hàng đầu tiên.

<Button Grid.Row="0" Grid.Column="0" Grid.RowSpan="2">

Span Button </Button>

Kết quả:

Vậy là các bạn đã nắm được những điểm cơ bản về Grid rồi đúng không. Chúng ta quay lại phần làm khung chính của game nhé. Vì trò chơi này được làm theo hướng dẫn của anh tauit nên phần cấu trúc của game cũng sẽ tương tự như vậy, và đó cũng là lý do tôi yêu cầu các bạn đọc bài tuts đó trước khi tiếp tục bài này. Với chương trình bằng C/C++, anh tauit dùng một ma trận 2 chiều kích thước 22×10, trong đó 4 hàng đầu tiên sẽ được dùng để lưu trữ các viên gạch lúc mới khởi tạo. Ở đây, tôi sử dụng ma trận 2 chiều với kích thước 24×12. Và Grid cũng giống như một ma trận 2 chiều, do đó chúng ta sẽ tạo ra một grid có 24 hàng và 12 cột dùng làm game board. Tôi muốn mỗi viên gạch sẽ là một hình vuông có cạnh là 25px, do đó grid của chúng ta sẽ có width = 25 x 12 = 300 và height = 25×24 = 600. Để tiện cho việc control sau này trong phần code behind, chúng ta sẽ đặt tên cho grid này là MainGrid. Dưới đây là phần code XAML:

<Grid Name="MainGrid"  Height="600" Width="300" Margin="5,0,0,5">
    <Grid.RowDefinitions>  <!--24 hàng-->  
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>   
        <RowDefinition></RowDefinition>
        ...
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>  <!--12 cột--> 
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
        ...
        <ColumnDefinition></ColumnDefinition>                
    </Grid.ColumnDefinitions>           
    ...
</Grid>

Bây giờ sẽ thêm thắt một vài chi tiết nữa để tạo ra game board như hình ở đầu bài viết này. Trước hết là một border bao quanh grid. Ngay bên dưới hàng </Grid.ColumnDefinitions>, chúng ta tạo ra một đối tượng Border. Các viên gạch sẽ chỉ được hiển thị từ hàng thứ 4 trở đi (vì 4 hàng đầu tiên sẽ dùng để khởi tạo viên gạch). Do đó, border của chúng ta sẽ được gắn vào hàng số 4 của grid, cột đầu tiên. Kéo dãn border này ra hết chiều ngang và chiều dọc còn lại của grid bằng thuộc tính RowSpan và ColumnSpan. Thay đổi giá trị một vài thuộc tính của đối tượng border này như BorderThickness, Background, BorderBrush… Ở đây tôi chọn BorderThickness bằng 4, để đảm bảo kích thước của grid bên trong, chúng ta sẽ thiết lập thuộc tính Margin=”-4”. Tương tự, chúng ta sử dụng các attached property của Grid để định vị các phần còn lại như sau:

<Grid ...

...

</Grid.ColumnDefinitions>

<Border Grid.Row="4" Grid.RowSpan="20" Grid.Column="0"

Grid.ColumnSpan="12" BorderBrush="Blue" Background="Black"

BorderThickness="4" Margin="-4"></Border> <!--Textblock ở giữa--> <TextBlock Grid.Column="2" Grid.ColumnSpan="8" Grid.Row="6"

Height="123" HorizontalAlignment="Center" Margin="0,2,0,0"

Name="playGame" TextAlignment="Center" VerticalAlignment="Top"

Width="200" Grid.RowSpan="5" FontFamily="Consolas" FontSize="20" FontWeight="Thin" Foreground="Yellow"

TextWrapping="Wrap" Text="Press Enter to play game"/> <!-- Phần hiển thị số điểm--> <Label Content="Score:" Height="28" Margin="0,22,0,0"

FontFamily="Consolas" FontSize="20" HorizontalAlignment="Left"

Grid.ColumnSpan="4" Grid.RowSpan="2" /> <TextBlock Name="score" TextAlignment="Right"

FontFamily="Courier New" FontWeight="ExtraBold"

FontStyle="Oblique" FontSize="24" Grid.Column="3"

Grid.ColumnSpan="3" Grid.Row="1" Grid.RowSpan="2"

Height="28" VerticalAlignment="Top">0

</TextBlock> <!-- Phần hiển thị level--> <Label Content="Level:" Height="28" Margin="0,22,0,0"

FontFamily="Consolas" FontSize="20" HorizontalAlignment="Left"

Grid.ColumnSpan="3" Grid.RowSpan="2" Grid.Row="1" /> <TextBlock Name="level" VerticalAlignment="Top"

TextAlignment="Right" FontFamily="Courier New"

FontWeight="ExtraBold" FontStyle="Oblique" FontSize="24"

Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="2"

Grid.RowSpan="2">0

</TextBlock> </Grid>

Lưu ý, chúng ta cần thiết phải đặt tên cho các textblock để sau này có thể xử lý trong phần code behind (giấu đi dòng chữ ở giữa grid, cập nhật điểm số, level).

Cuối cùng là phía bên phải của cửa sổ chương trình của chúng ta. Phần này dùng để hiển thị các ô gạch tiếp theo sẽ rơi xuống. Tương tự, chúng ta cũng sử dụng Grid để quản lý các khối gạch. Grid này sẽ có 5 hàng, 5 cột, mỗi ô có kích thước 25px. Tôi muốn hiển thị 3 khối gạch tiếp theo sẽ rơi xuống, do đó tôi tạo ra 3 Grid như vậy và đặt bên trong một StackPanel.

<!-- Right side-->
<StackPanel Margin="30,0,0,0" >
...

</StackPanel>

Phần tử đầu tiên đặt trong stack panel này là một Label có nội dung là “Next blocks”. Font consolas, FontSize là 20, khoảng cách rìa bên trái là 5px và ở trên là 100px (để ngang bằng với border trong mainGrid):

<Label Content="Next Blocks" Height="28"

FontFamily="Consolas" FontSize="20" Margin="5,100,0,0"/>

Sau đó sẽ là 3 Grid với tên gọi lần lượt là rightGrid1, rightGrid2 và rightGrid3:

<Grid Name="rightGrid1" Margin="0,10,0,0" Width="125" Height="125">
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
</Grid>

rightGrid2 và rightGrid3 cũng tương tự như rightGrid1. Như vậy chúng ta đã xong phần tạo ra giao diện cho game xếp gạch. Hoàn toàn không dính dáng gì đến code C# đúng không. Đó chính là điểm mạnh của WPF mà tôi đã từng đề cập ở các bài viết trước.

Các bạn có thể download file xaml tại đây!