WPF/UWP的Grid佈局竟然有Bug,還不止一個!瞭解Grid中那些未定義的佈局規則

傑克.陳發表於2018-09-21
原文:WPF/UWP 的 Grid 佈局竟然有 Bug,還不止一個!瞭解 Grid 中那些未定義的佈局規則
版權宣告:本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名呂毅(包含連結:http://blog.csdn.net/wpwalter/),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請與我聯絡(walter.lv@qq.com)。 https://blog.csdn.net/WPwalter/article/details/80371262

只要你用 XAML 寫程式碼,我敢打賭你一定用各種方式使(nuè)用(dài)過 Grid。不知你有沒有在此過程中看到過 Grid 那些匪夷所思的佈局結果呢?

本文將帶你來看看 Grid 佈局中的 Bug。


無限空間下的比例

先上一段程式碼,直接複製到你的試驗專案中執行:

<Canvas>
    <Grid Height="100">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="2*" />
        </Grid.ColumnDefinitions>
        <Border Grid.Column="0" Background="CornflowerBlue" Width="150" />
        <Border Grid.Column="1" Background="Tomato" Width="150" />
        <Border Grid.Column="2" Background="Teal" Width="150" />
    </Grid>
</Canvas>

第一列固定 100,第二列佔 1 個比例的 *,第三列佔 2 個比例的 *。你覺得最終的效果中,第二個 Border 和第三個 Border 的可見尺寸分別是多少呢?

F5

預料到了嗎?雖然第二列和第三列的比例是 1:2,但最終的可見比例卻是 1:1。

這裡是有破綻的,因為你可能會懷疑第三列其實已經是第二列的兩倍,只是右側是空白,看不出來。那麼現在,我們去掉 Canvas,改用在父 Grid 中右對齊,也就是如下程式碼:

<Grid HorizontalAlignment="Right">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>
    <Border Grid.Column="0" Background="CornflowerBlue" Width="150" />
    <Border Grid.Column="1" Background="Tomato" Width="150" />
    <Border Grid.Column="2" Background="Teal" Width="150" />
</Grid>

執行後,你會發現最右側是沒有空白的,也就是說第二列和第三列確實不存在 1:2 的比例——它們是等寬的。

那麼那一段失去的空間去哪裡了呢?讓我們縮小視窗:

縮小視窗

竟然在左側還有剩餘空間的情況下,右側就開始壓縮元素空間了!我們能說那段丟失的一個 * 長度的空白到左邊去了嗎?顯然不能。

不過,我們能夠猜測,壓縮右側元素開始於最小 1:2 的比例正好不足時出現。

剛好不夠分的比例

右對齊能夠幫助我們區分右側是否真的佔有空間。那麼我們繼續右對齊做試驗。

現在,我們將第二列的 Border 做成跨第二和第三兩列的元素。第三列的 Border 放到第二列中。(也就是說,我們第三列不放元素了。)

<Grid HorizontalAlignment="Right">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>
    <Border Grid.Column="0" Background="CornflowerBlue" Width="150" />
    <Border Grid.Column="1" Grid.ColumnSpan="2" Background="Tomato" Width="150" />
    <Border Grid.Column="1" Background="Teal" Width="150" />
</Grid>

執行看看,在得知前一節現象的情況下,新的現象並沒有出現多大的意外。第三列憑空消失,第二列與之之間依然失去了 1:2 的比例關係。

然而,我們還可以縮小視窗。

為什麼在縮小視窗的時候突然間出現了那個紅色的 Border?為什麼在紅色 Border 的右邊還留有空白?

如果說第一節中我們認識到右對齊時右邊剩餘的空白空間會丟掉,那麼為什麼此時右邊剩餘的空白空間會突然出現?

我試著稍微增加第二個 Border 的寬度,突然間,剛剛縮小視窗時的行為也能復現!

自動尺寸也能玩比例

現在,我們拋棄之前的右對齊測試方法,也不再使用預期按比例劃分空間的 *。我們使用 Auto 來實現比例功能。

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Border Width="159" Grid.ColumnSpan="3" HorizontalAlignment="Center" Background="PaleGreen" />
    <Border Width="28" HorizontalAlignment="Left" Background="#7FFF6347" />
    <Border Width="51" Grid.Column="1" HorizontalAlignment="Center" Background="#7FC71585" />
    <Border Width="28" Grid.Column="2" HorizontalAlignment="Right" Background="#7F008080" />
</Grid>

具體說來,我們有四個 Border 了,放在 Auto 尺寸的三列中。第一個 Border 橫跨三列,尺寸比其他總和都長,達到了 159;剩下的三個 Border 各佔一列,其中兩邊等長,中間稍長。

那麼實際佈局中各列是怎麼分的呢?以下是設計器為我們顯示的列寬:

466946 是怎麼來的?莫非是 46:6928:51 相同?然而實際計算結果卻並不是!

可萬一這是計算誤差呢?

那麼我們再來看看三個 Border 的另外兩組值:50:50:5025:50:25


50:50:50


25:50:25

50:50:50 最終得到的是相同比例,但是 25:50:25 得到的列寬比例與 1:2 相去甚遠。也就是說,其實 Grid 內部並沒有按照元素所需的尺寸來按比例計算列寬。

相同比例也能有不同尺寸

在上一節的試驗中,不管比例如何,至少相同的設定尺寸帶來了相同的最終可見尺寸。然而,就算是這一點,也是能被顛覆的。

現在,我們將 3 列換成 4 列,Border 數量換成 6 個。

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Border Width="159" Grid.ColumnSpan="3" HorizontalAlignment="Center" Background="PaleGreen" />
    <Border Width="159" Grid.Column="1" Grid.ColumnSpan="3" HorizontalAlignment="Center" Background="PaleGreen" />
    <Border Width="28" HorizontalAlignment="Left" Background="#7FFF6347" />
    <Border Width="51" Grid.Column="1" HorizontalAlignment="Center" Background="#7FC71585" />
    <Border Width="51" Grid.Column="2" HorizontalAlignment="Center" Background="#7FC71585" />
    <Border Width="28" Grid.Column="3" HorizontalAlignment="Right" Background="#7F008080" />
</Grid>

具體來說,第一個 Border 跨前三列,第二個 Border 跨後三列,跟前一節的長 Border 一樣長。第三和第六個 Border 分在兩邊,與之前的短 Border 一樣短。中間的兩個 Border 與之前中間的 Border 一樣長。就像下圖所示的這樣。

那麼此時佈局出來的列寬是多少呢?


▲ 32:65:65:39

等等!那個 39 是怎麼來的?如果前一節裡相等尺寸的 Border 會得到相等尺寸的列寬,那麼這裡也將顛覆!事實上,即便此時列寬比例與元素所需比例一致,在這種佈局下也是有無窮多個解的。WPF 只是從這無窮多個解中挑選了一個出來——而且,還無法解釋!

總結 Grid 未定義的規則

總而言之,言而總之,Grid 佈局在特殊情況下是有一些不合常理的。我稱之為“未定義的規則”。這些未定義的規則總結起來有以下三點:

  1. 在無窮大布局空間時的 * 的比例
  2. 在跨多列布局時 * 的比例
  3. 在全 Auto 尺寸時各列尺寸

不過你也可能會吐槽我的用法不對,可是,作為一個連表現行為都公開的 API,其行為也是 API 的一部分,應該具有明確可追溯可文件化的行為;而不是由使用者去探索,最終無法猜測可發生事情的行為。

微軟沒有任何官方文件公開了這些詭異的行為,我也沒有在任何第三方資料中找到這樣的行為(這些都是我自己總結的)。我認為,微軟沒有為此公開文件是因為行為太過詭異,無法編寫成文件!

你可能還會質疑,可以去 Reference Source 查閱 Grid 佈局的原始碼,那樣就能解釋這些詭異的行為了。確實如此,那裡是這一切詭異佈局背後的罪魁禍首。

我閱讀過 Grid 的佈局原始碼,但沒能全部理解,而且在閱讀的過程中發現了一些微軟官方承認的 Bug(我也沒有能力去解決它)。

不過,我整整三天的時間寫了一個全新的 Grid 佈局演算法(感謝 @林德熙 抽出時間跟我探討 Grid 的佈局演算法)。在新的演算法中,對於微軟公開的 Grid 佈局行為,我跟它的表現是一樣的。對於本文中提到的各種 Bug,我找不到手段實現跟它一模一樣的佈局結果,但是,我可以文件化地完全確定 Grid 整個佈局的所有行為。包括以上所有我認為的“未定義的規則”。

Grid 佈局演算法的原始碼在 GitHub 上,我提交給了 Avalonia:A new grid layout algorithm to improve performance and fix some bugs by walterlv · Pull Request #1517 · AvaloniaUI/Avalonia


相關文章