上一篇介紹了跨平臺移動開發解決方案Flutter以及Flutter開發環境的搭建,由於Flutter開發使用的是Dart語言,故本篇記錄的是Dart語言的語法基礎,希望跟小夥伴們一起溫故知新。
Dart語言簡介
Dart是Google推出的一門程式語言,最初是希望取代Javascript執行在瀏覽器端,後來慢慢發展成可以開發Android、iOS和Web端App的一門高質量的程式語言,目前Dart的版本是Dart2,官網是:www.dartlang.org/
在Dart官方網站上,對於Dart的描述如下:
Developers at Google and elsewhere use Dart to create high-quality, mission-critical apps for iOS, Android, and the web. With features aimed at client-side development, Dart is a great fit for both mobile and web apps.
Google和其他地方的一些開發者使用Dart語言為Android、iOS和web構建高質量,關鍵任務的應用程式,針對客戶端開發的特點,Dart非常適合移動和Web應用程式。
Dart語言的特性
Productive(豐富多產的)
Dart’s syntax is clear and concise, its tooling simple yet powerful. Sound typing helps you to identify subtle errors early. Dart has battle-hardened core libraries and an ecosystem of thousands of packages.
Dart的語法清晰明瞭,工具簡單但功能強大。Sound typing有助於早期識別細微的錯誤。Dart擁有久經沙場的核心庫和數以千計的生態系統。
Fast(快速的)
Dart provides optimizing ahead-of-time compilation to get predictably high performance and fast startup across mobile devices and the web.
Dart提供提前優化編譯,以在移動裝置和Web上獲得可預測的高效能和快速啟動。
Portable(可移植的)
Dart compiles to ARM and x86 code, so that Dart mobile apps can run natively on iOS, Android, and beyond. For web apps, Dart transpiles to JavaScript.
Dart可編譯成ARM和X86程式碼,這樣Dart移動應用程式可以在iOS、Android和其他地方執行。對於Web應用程式,DART可編譯成JavaScript。
Approachable(親切的)
Dart is familiar to many existing developers, thanks to its unsurprising object orientation and syntax. If you already know C++, C#, or Java, you can be productive with Dart in just a few days.
Dart對於許多現有的開發人員來說是熟悉的,這得益於其令人驚訝的物件定位和語法。如果你已經知道C++,C語言,或者Java,你可以在短短几天內用Dart來開發。
Reactive(反應式的)
Dart is well-suited to reactive programming, with support for managing short-lived objects—such as UI widgets—through Dart’s fast object allocation and generational garbage collector. Dart supports asynchronous programming through language features and APIs that use Future and Stream objects.
Dart非常適合於反應式程式設計,支援通過Dart的快速物件分配和代垃圾收集器來管理諸如UI小部件之類的短命物件。Dart通過使用未來和流物件的語言特徵和API支援非同步程式設計。
Dart語法簡介
關於Dart的語法,如果你熟悉Java,應該很快能掌握Dart,官網上對於Dart的語法也有詳細介紹,不過是全英文的,如果對英文沒有什麼閱讀障礙,可以直接移步官方文件。
為了瞭解Dart的語法基礎,這裡我們使用Android Studio作為開發工具(你也可以使用dartpad來執行程式碼,它是一個基於瀏覽器的dart執行時環境),如果你按照上一篇文章中搭建好了Flutter開發環境,那麼可以直接在Android Studio中新建Flutter專案,如下圖所示:
新建立的Flutter專案,Dart程式碼主要在lib/main.dart
檔案中,由於本篇主要是講Dart的語法,故暫時不看main.dart檔案,在lib目錄下我們建立一個新的.dart
檔案demo.dart
,如下圖所示:
在新建的demo.dart
檔案中,輸入如下程式碼:
// Define a function.
printInteger(int aNumber) {
print('The number is $aNumber.'); // Print to console.
}
// This is where the app starts executing.
main() {
var number = 42; // Declare and initialize a variable.
printInteger(number); // Call a function.
}
複製程式碼
然後在程式碼編輯區域滑鼠右鍵,選擇Run demo.dart
,即可執行一個最簡單的dart程式,如下圖所示:
-
Dart中單行註釋使用
//
,Dart同時支援多行註釋和文件註釋,可以點選這裡檢視更多 -
int
是Dart中的一種資料型別,同時還有其他的一些內建資料型別如String
List
bool
等 -
控制檯輸出使用
print
語句 -
字串使用單引號或雙引號均可,如'hello', "hello"
-
字串插入可以使用類似
$name
或${name}
的語法,比如下面的程式碼:var name = 'zhangsan'; print("hello, I am $name"); int a = 10, b = 20; print("$a + $b = ${a + b}"); 複製程式碼
如果使用
${name}
這種方式,大括號中可以是表示式 -
你可能已經注意到了,Dart的變數型別是可選的,你可以為某個變數指定型別,或者使用
var
定義變數,Dart會自動推斷變數的型別
重要概念
當你在學習Dart語言時,下面的這些事實和概念請牢記於心:
- 在Dart中,一切都是物件,一切物件都是class的例項,哪怕是數字型別、方法甚至null都是物件,所有的物件都是繼承自
Object
- 雖然Dart是強型別語言,但變數型別是可選的因為Dart可以自動推斷變數型別
- Dart支援範型,
List<int>
表示一個整型的資料列表,List<dynamic>
則是一個物件的列表,其中可以裝任意物件 - Dart支援頂層方法(如
main
方法),也支援類方法或物件方法,同時你也可以在方法內部建立方法 - Dart支援頂層變數,也支援類變數或物件變數
- 跟Java不同的是,Dart沒有
public
protected
private
等關鍵字,如果某個變數以下劃線(_
)開頭,代表這個變數在庫中是私有的,具體可以看這裡 - Dart中變數可以以字母或下劃線開頭,後面跟著任意組合的字元或數字
- 有時重要的是某事是一個表達還是一個陳述,所以這兩個詞的精確性是有幫助的
- Dart工具可以報告兩種問題:警告和錯誤。警告只是指示程式碼可能無法工作,但它們不會阻止程式執行。錯誤可以是編譯時,也可以是執行時發生。編譯時錯誤根本不允許程式碼執行;執行時錯誤導致程式碼執行時引發異常。
變數
變數定義
以下程式碼是Dart中定義變數的方法:
main() {
var a = 1;
int b = 10;
String s = "hello";
dynamic c = 0.5;
}
複製程式碼
你可以明確指定某個變數的型別,如int
bool
String
,也可以用var
或 dynamic
來宣告一個變數,Dart會自動推斷其資料型別。
變數的預設值
注意:沒有賦初值的變數都會有預設值null
final和const
如果你絕不想改變一個變數,使用final
或const
,不要使用var
或其他型別,一個被final
修飾的變數只能被賦值一次,一個被const
修飾的變數是一個編譯時常量(const常量毫無疑問也是final常量)。可以這麼理解:final
修飾的變數是不可改變的,而const
修飾的表示一個常量。
注意:例項變數可以是final的但不能是const的
下面用程式碼說明:
final String name = 'zhangsan';
name = 'lisi'; // 編譯不通過,被final修飾的是常量,不可重新賦值
const a = 0;
a = 1; // 錯誤
複製程式碼
final
和const
的區別:
- 區別一:
final
要求變數只能初始化一次,並不要求賦的值一定是編譯時常量,可以是常量也可以不是。而const
要求在宣告時初始化,並且賦值必需為編譯時常量。 - 區別二:
final
是惰性初始化,即在執行時第一次使用前才初始化。而const
是在編譯時就確定值了。
內建資料型別
Dart有如下幾種內建的資料型別:
- numbers
- strings
- booleans
- lists(或者是arrays)
- maps
- runes(UTF-32字符集的字元)
- symbols
下面用一段程式碼來演示以上各類資料型別:
main() {
// numbers
var a = 0;
int b = 1;
double c = 0.1;
// strings
var s1 = 'hello';
String s2 = "world";
// booleans
var real = true;
bool isReal = false;
// lists
var arr = [1, 2, 3, 4, 5];
List<String> arr2 = ['hello', 'world', "123", "456"];
List<dynamic> arr3 = [1, true, 'haha', 1.0];
// maps
var map = new Map();
map['name'] = 'zhangsan';
map['age'] = 10;
Map m = new Map();
m['a'] = 'a';
//runes,Dart 中 使用runes 來獲取UTF-32字符集的字元。String的 codeUnitAt and codeUnit屬性可以獲取UTF-16字符集的字元
var clapping = '\u{1f44f}';
print(clapping); // 列印的是拍手emoji的表情
// symbols
print(#s == new Symbol("s")); // true
}
複製程式碼
函式
函式的返回值
Dart是一個物件導向的程式語言,所以即使是函式也是一個物件,也有一種型別Function
,這就意味著函式可以賦值給某個變數或者作為引數傳給另外的函式。雖然Dart推薦你給函式加上返回值,但是不加返回值的函式同樣可以正常工作,另外你還可以用=>
代替return
語句,比如下面的程式碼:
// 宣告返回值
int add(int a, int b) {
return a + b;
}
// 不宣告返回值
add2(int a, int b) {
return a + b;
}
// =>是return語句的簡寫
add3(a, b) => a + b;
main() {
print(add(1, 2)); // 3
print(add2(2, 3)); // 5
print(add3(1, 2)); // 3
}
複製程式碼
命名引數、位置引數、引數預設值
命名引數
使用花括號將函式的引數括起來就是定義了命名引數,如下面的程式碼所示:
sayHello({String name}) {
print("hello, my name is $name");
}
sayHello2({name: String}) {
print("hello, my name is $name");
}
main() {
// 列印 hello, my name is zhangsan
sayHello(name: 'zhangsan');
// 列印 hello, my name is wangwu
sayHello2(name: 'wangwu');
}
複製程式碼
可以看到,定義命名引數時,你可以以{type paramName}
或者{paramName: type}
兩種方式宣告引數,而呼叫命名引數時,需要以funcName(paramName: paramValue)
的形式呼叫。
命名引數的引數並不是必須的,所以上面的程式碼中,如果呼叫sayHello()
不帶任何引數,也是可以的,只不過最後列印出來的結果是:hello, my name is null
,在Flutter開發中,你可以使用@required
註解來標識一個命名引數,這代表該引數是必須的,你不傳則會報錯,比如下面的程式碼:
const Scrollbar({Key key, @required Widget child})
複製程式碼
位置引數
使用中括號[]
括起來的引數是函式的位置引數,代表該引數可傳可不傳,位置引數只能放在函式的引數列表的最後面,如下程式碼所示:
sayHello(String name, int age, [String hobby]) { // 位置引數可以有多個,比如[String a, int b]
StringBuffer sb = new StringBuffer();
sb.write("hello, this is $name and I am $age years old");
if (hobby != null) {
sb.write(", my hobby is $hobby");
}
print(sb.toString());
}
main() {
// hello, this is zhangsan and I am 20 years old
sayHello("zhangsan", 20);
// hello, this is zhangsan and I am 20 years old, my hobby is play football
sayHello("zhangsan", 20, "play football");
}
複製程式碼
引數預設值
你可以為命名引數或者位置引數設定預設值,如下程式碼所示:
// 命名引數的預設值
int add({int a, int b = 3}) { // 不能寫成:int add({a: int, b: int = 3})
return a + b;
}
// 位置引數的預設值
int sum(int a, int b, [int c = 3]) {
return a + b + c;
}
複製程式碼
main()函式
不論在Dart
還是Flutter
中,必須都需要一個頂層的main()
函式,它是整個應用的入口函式,main()
函式的返回值是void
,還有一個可選的引數,引數型別是List<String>
。
函式作為一類物件
你可以將一個函式作為引數傳給另一個函式,比如下面的程式碼:
printNum(int a) {
print("$a");
}
main() {
// 依次列印:
// 1
// 2
// 3
var arr = [1, 2, 3];
arr.forEach(printNum);
}
複製程式碼
你也可以將一個函式賦值給某個變數,比如下面的程式碼:
printNum(int a) {
print("$a");
}
main() {
var f1 = printNum;
Function f2 = printNum;
var f3 = (int a) => print("a = $a");
f1(1);
f2(2);
f3(6);
}
複製程式碼
匿名函式
大多數函式都是有名稱的,比如main()
printName()
等,但是你也可以寫匿名函式,如果你對Java
比較熟悉,那下面的Dart
程式碼你肯定也不會陌生:
test(Function callback) {
callback("hello");
}
main() {
test((param) {
// 列印hello
print(param);
});
}
複製程式碼
匿名函式類似於Java
中的介面,往往在某個函式的引數為函式時使用到。
函式返回值
所有的函式都有返回值,如果沒有指定return
語句,那麼該函式的返回值為null
。
運算子
Dart
中的運算子與Java
中的類似,比如++a
a == b
b ? a : b
,但是也有一些與Java
不太一樣的運算子,下面用程式碼說明:
main() {
// 與Java相同的運算子操作
int a = 1;
++a;
a++;
var b = 1;
print(a == b); // false
print(a * b); // 3
bool real = false;
real ? print('real') : print('not real'); // not real
print(real && a == b); // false
print(real || a == 3); // true
print(a != 2); // true
print(a <= b); // false
var c = 9;
c += 10;
print("c = $c"); // c = 19
print(1<<2); // 4
// 與Java不太一樣的運算子操作
// is運算子用於判斷一個變數是不是某個型別的資料
// is!則是判斷變數不是某個型別的資料
var s = "hello";
print(s is String); // true
var num = 6;
print(num is! String); // true
// ~/才是取整運算子,如果使用/則是除法運算,不取整
int k = 1;
int j = 2;
print(k / j); // 0.5
print(k ~/ j); // 0
// as運算子類似於Java中的cast操作,將一個物件強制型別轉換
(emp as Person).teach();
// ??=運算子 如果 ??= 運算子前面的變數為null,則賦值,否則不賦值
var param1 = "hello", param2 = null;
param1 ??= "world";
param2 ??= "world";
print("param1 = $param1"); // param1 = hello
print("param2 = $param2"); // param2 = world
// ?.運算子
var str1 = "hello world";
var str2 = null;
print(str1?.length); // 11
print(str2?.length); // null
print(str2.length); // 報錯
}
複製程式碼
..運算子(級聯操作)
如果你對Java中的建造者模式比較熟悉的話,Dart
中的..
運算子也很好理解,先看下面的程式碼:
class Person {
eat() {
print("I am eating...");
}
sleep() {
print("I am sleeping...");
}
study() {
print("I am studying...");
}
}
main() {
// 依次列印
// I am eating...
// I am sleeping...
// I am studying...
new Person()..eat()
..sleep()
..study();
}
複製程式碼
可以看到,使用..
呼叫某個物件的方法(或者成員變數)時,返回值是這個物件本身,所以你可以接著使用..
呼叫這個物件的其他方法,這不就類似於Java
中的建造者模式,每次build某個屬性時,都返回一個this
物件嗎。
控制流程
if / else
switch
for /while
try / catch
語句跟Java
中都類似,try / catch
語句可能稍有不同,下面用一段程式碼說明:
main() {
// if else語句
int score = 80;
if (score < 60) {
print("so bad!");
} else if (score >= 60 && score < 80) {
print("just so so!");
} else if (score >= 80) {
print("good job!");
}
// switch語句
String a = "hello";
// case語句中的資料型別必須是跟switch中的型別一致
switch (a) {
case "hello":
print("haha");
break;
case "world":
print("heihei");
break;
default:
print("WTF");
}
// for語句
List<String> list = ["a", "b", "c"];
for (int i = 0; i < list.length; i++) {
print(list[i]);
}
for (var i in list) {
print(i);
}
// 這裡的箭頭函式引數必須用圓括號擴起來
list.forEach((item) => print(item));
// while語句
int start = 1;
int sum = 0;
while (start <= 100) {
sum += start;
start++;
}
print(sum);
// try catch語句
try {
print(1 ~/ 0);
} catch (e) {
// IntegerDivisionByZeroException
print(e);
}
try {
1 ~/ 0;
} on IntegerDivisionByZeroException { // 捕獲指定型別的異常
print("error"); // 列印出error
} finally {
print("over"); // 列印出over
}
}
複製程式碼
類(Class)
類的定義與構造方法
Dart
中的類沒有訪問控制,所以你不需要用private
, protected
, public
等修飾成員變數或成員函式,一個簡單的類如下程式碼所示:
class Person {
String name;
int age;
String gender;
Person(this.name, this.age, this.gender);
sayHello() {
print("hello, this is $name, I am $age years old, I am a $gender");
}
}
複製程式碼
上面的Person
類中有3個成員變數,一個構造方法和一個成員方法,看起來比較奇怪的是Person
的構造方法,裡面傳入的3個引數都是this.xxx
,而且沒有大括號{}
包裹的方法體,這種語法是Dart比較獨特而簡潔的構造方法宣告方式,它等同於下面的程式碼:
Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
複製程式碼
要呼叫Person
類的成員變數或成員方法,可以用下面的程式碼:
var p = new Person("zhangsan", 20, "male");
p.sayHello(); // hello, this is zhangsan, I am 20 years old, I am a male
p.age = 50;
p.gender = "female";
p.sayHello(); // hello, this is zhangsan, I am 50 years old, I am a female
複製程式碼
由於Dart
中的類沒有訪問控制許可權,所以你可以直接用obj.var
的方式訪問一個物件的成員變數。
類除了有跟類名相同的構造方法外,還可以新增命名的構造方法,如下程式碼所示:
class Point {
num x, y;
Point(this.x, this.y);
// 類的命名構造方法
Point.origin() {
x = 0;
y = 0;
}
}
main() {
// 呼叫Point類的命名構造方法origin()
var p = new Point.origin();
var p2 = new Point(1, 2);
}
複製程式碼
Dart
中使用extends
關鍵字做類的繼承,如果一個類只有命名的構造方法,在繼承時需要注意,如下程式碼:
class Human {
String name;
Human.fromJson(Map data) {
print("Human's fromJson constructor");
}
}
class Man extends Human {
Man.fromJson(Map data) : super.fromJson(data) {
print("Man's fromJson constructor");
}
}
複製程式碼
由於Human
類沒有預設構造方法,只有一個命名構造方法fromJson
,所以在Man
類繼承Human
類時,需要呼叫父類的fromJson
方法做初始化,而且必須使用Man.fromJson(Map data) : super.fromJson(data)
這種寫法,而不是像Java
那樣將super
寫到花括號中。
有時候你僅僅只是在某個類的構造方法中,呼叫這個類的另一個構造方法,你可以這麼寫:
class Point {
num x, y;
Point(this.x, this.y);
// 命名構造方法呼叫了預設的構造方法
Point.alongXAxis(num x) : this(x, 0);
}
複製程式碼
類的成員方法
一個類的成員方法是一個函式,為這個類提供某些行為。上面的程式碼中已經有了一些類的成員方法的定義,這些定義方式跟Java
很類似,你可以為某個類的成員變數提供getter/setter
方法,如下程式碼:
class Rectangle {
num left, top, width, height;
// 構造方法傳入left, top, width, height幾個引數
Rectangle(this.left, this.top, this.width, this.height);
// right, bottom兩個成員變數提供getter/setter方法
num get right => left + width;
set right(num value) => left = value - width;
num get bottom => top + height;
set bottom(num value) => top = value - height;
}
複製程式碼
抽象類和抽象方法
使用abstract修飾一個類,則這個類是抽象類,抽象類中可以有抽象方法和非抽象方法,抽象方法沒有方法體,需要子類去實現,如下程式碼:
abstract class Doer {
// 抽象方法,沒有方法體,需要子類去實現
void doSomething();
// 普通的方法
void greet() {
print("hello world!");
}
}
class EffectiveDoer extends Doer {
// 實現了父類的抽象方法
void doSomething() {
print("I'm doing something...");
}
}
複製程式碼
運算子過載
Dart中有類似於C++中的運算子過載語法,比如下面的程式碼定義了一個向量類,過載了向量的+
-
運算:
class Vector {
num x, y;
Vector(this.x, this.y);
Vector operator +(Vector v) => new Vector(x + v.x, y + v.y);
Vector operator -(Vector v) => new Vector(x - v.x, y - v.y);
printVec() {
print("x: $x, y: $y");
}
}
main() {
Vector v1 = new Vector(1, 2);
Vector v2 = new Vector(3, 4);
(v1 - v2).printVec(); // -2, -2
(v1 + v2).printVec(); // 4, 6
}
複製程式碼
列舉類
使用enum
關鍵字定義一個列舉類,這個語法跟Java
類似,如下程式碼:
enum Color { red, green, blue }
複製程式碼
mixins
mixins是一個重複使用類中程式碼的方式,比如下面的程式碼:
class A {
a() {
print("A's a()");
}
}
class B {
b() {
print("B's b()");
}
}
// 使用with關鍵字,表示類C是由類A和類B混合而構成
class C = A with B;
main() {
C c = new C();
c.a(); // A's a()
c.b(); // B's b()
}
複製程式碼
靜態成員變數和靜態成員方法
// 類的靜態成員變數和靜態成員方法
class Cons {
static const name = "zhangsan";
static sayHello() {
print("hello, this is ${Cons.name}");
}
}
main() {
Cons.sayHello(); // hello, this is zhangsan
print(Cons.name); // zhangsan
}
複製程式碼
泛型(Generics)
Java
和C++
語言都有泛型,Dart
語言也不例外,使用泛型有很多好處,比如:
- 正確指定泛型型別會產生更好的生成程式碼。
- 泛型可以減小程式碼的複雜度
Dart
內建的資料型別List
就是一個泛型資料型別,你可以往List
中塞任何你想的資料型別比如整型、字串、布林值等
關於Dart
更多的泛型知識點,可以檢視這裡。
Dart庫(Libraries)
Dart
目前已經有很多的庫提供給開發者,許多功能不需要開發者自己去實現,只需要匯入對應的包即可,使用import
語句來匯入某個包,比如下面的程式碼:
import 'dart:html';
複製程式碼
如果你想匯入自己寫的某個程式碼檔案,使用相對路徑即可,例如當前有一個demo.dart
檔案,跟該檔案同級目錄下有個util.dart
檔案,檔案程式碼如下:
// util.dart檔案內容
int add(int a, int b) {
return a + b;
}
複製程式碼
在demo.dart
檔案中如果要引用util.dart
檔案,使用下面的方式匯入:
// demo.dart
import './util.dart';
main() {
print(add(1, 2));
}
複製程式碼
你可以使用as
關鍵字為匯入的某個包設定一個字首,或者說別名,比如下面的程式碼:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// Uses Element from lib1.
Element element1 = Element();
// Uses Element from lib2.
lib2.Element element2 = lib2.Element();
複製程式碼
你也可以在匯入包時使用show
hide
關鍵字來匯入某個包中的部分功能,比如下面的程式碼:
// 只匯入foo
import 'package:lib1/lib1.dart' show foo;
// 匯入除了foo的所有其他部分
import 'package:lib2/lib2.dart' hide foo;
複製程式碼
匯入包時使用deferred as
可以讓這個包懶載入,懶載入的包只會在該包被使用時得到載入,而不是一開始就載入,比如下面的程式碼:
import 'package:greetings/hello.dart' deferred as hello;
複製程式碼
非同步
Dart
提供了類似ES7中的async
await
等非同步操作,這種非同步操作在Flutter開發中會經常遇到,比如網路或其他IO操作,檔案選擇等都需要用到非同步的知識。
async
和await
往往是成對出現的,如果一個方法中有耗時的操作,你需要將這個方法設定成async
,並給其中的耗時操作加上await
關鍵字,如果這個方法有返回值,你需要將返回值塞到Future
中並返回,如下程式碼所示:
Future checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}
複製程式碼
下面的程式碼使用Dart
從網路獲取資料並列印出來:
import 'dart:async';
import 'package:http/http.dart' as http;
Future<String> getNetData() async{
http.Response res = await http.get("http://www.baidu.com");
return res.body;
}
main() {
getNetData().then((str) {
print(str);
});
}
複製程式碼
關於Dart
非同步操作,可以檢視這篇文章瞭解更多。
結束語
本篇部落格較長,主要是對官方文件的一個翻譯(大部分),如果你對英文閱讀沒有太大障礙,建議直接檢視官方的英文文件,希望各位都能愉快的學習Dart
和Flutter
。
參考
我的開源專案
- 基於Google Flutter的開源中國客戶端,希望大家給個Star支援一下,原始碼:
- 基於Flutter的俄羅斯方塊小遊戲,希望大家給個Star支援一下,原始碼:
上一篇 | 下一篇 |
---|---|
從0開始寫一個基於Flutter的開源中國客戶端(1) ——Flutter簡介及開發環境搭建 |
從0開始寫一個基於Flutter的開源中國客戶端(3) ——初識Flutter & 常用的Widgets |