Python 之 tkinter 学习笔记

本文最后更新于:2 个月前

Python 之 tkinter 学习笔记

前言

最近有个小需求需要实现,最后要给出一个 GUI 界面,想了想还是不用 c++ 写 MFC 了,因为还涉及到网络编程,感觉还是简单事情简单做,然后转手写 python,刚开始想尝试 pyqt,但感觉好像还是有点麻烦,本来就只是做个插件,最后就大概看了一下内置的 tkinter,一边写一边学也马上就上手了,总的来说感觉还是简单的。

tkinter 简介

Tkinter 是 Tk GUI 工具包的 Python 绑定包。它是 Tk GUI 工具包的标准 Python 接口,并且是 Python 的业界标准 GUI 工具包。

创建一个窗口

由于 python 内置了 tkinter 因此我们不需要安装额外的库,直接导入即可

1
import tkinter as tk

下面是一个简单的示例,它创建了一个窗口,设置窗口标题,并设置窗口大小和位置

1
2
3
4
5
6
7
8
9
10
11
12
13
import tkinter as tk

# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300+500+300')

# 主窗口循环显示
window.mainloop()

代码很简单,也不难理解,效果如下:

很多时候,为了美观,我们需要窗口显示在屏幕中样,这时候我们可以通过 winfo_screenwidth()winfo_screenheight() 获取显示区域的宽度和高度,然后将窗口显示在屏幕中央。

1
2
3
4
5
6
7
8
9
10
11
screenWidth = window.winfo_screenwidth()  # 获取显示区域的宽度
screenHeight = window.winfo_screenheight() # 获取显示区域的高度

width = 500 # 设定窗口宽度
height = 300 # 设定窗口高度

left = (screenWidth - width) / 2
top = (screenHeight - height) / 2

# 宽度 x 高度 + x偏移 + y偏移
window.geometry("%dx%d+%d+%d" % (width, height, left, top))

添加窗口部件

窗口部件简介

tkinter 同样有许多小部件,例如按钮,文本框,输入框等,将这些组件拼接,就可以得到一个比较完整的桌面程序。

tkinter 类 元素 说明
Button 按钮 在程序中显示按钮
Canvas 画布 提供绘制功能
Checkbutton 多选框 在程序中显示多选框
Combobox 下拉框 显示下拉框
Entry 输入框 显示单行文本内容
Frame 框架 用于放置其他窗口部件
Label 标签 显示文本或位图
Listbox 列表框 显示选择列表
Menu 菜单 显示菜单栏
Message 消息框 类似与标签,可以显示多行文本
Radiobutton 单选按钮 显示单选按钮
Scale 进度条 线性滑块组件
Scrollbar 滚动条 显示一个滚动条
Text 文本框 显示多行文本
messagebox 消息框 弹出一个消息框

设置组件位置

说完了部件之后,我们同样还要考虑放置部件的位置。

tkinter 有三种布局管理方式:

  • pack()
  • grid()
  • place()

pack()

pack() 是最常用的布局,不需要指定具体位置,当然也可以通过指定位置,边距来实现复杂的布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import tkinter as tk

# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

label_1 = tk.Label(window, text='label_1', bg="green").pack()
# 填充 x 轴
label_2 = tk.Label(window, text='label_2', bg="red").pack(fill=tk.X)
# 填充 x 轴,x 轴 y 轴边距分别为 100,50
label_3 = tk.Label(window, text='label_3', bg="yellow").pack(fill=tk.X, padx=100, pady=50)

# 主窗口循环显示
window.mainloop()

效果如下:

grid()

Grid 在很多场景下是最好用的布局方式,它把控件位置作为一个二维表结构来维护,使用一个行列结构来定位每一个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import tkinter as tk

# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

for i in range(9):
if i % 2 == 0:
bg_color = "white"
else:
bg_color = "gray"
label = tk.Label(window, text=str(i + 1), bg=bg_color, width=6, height=3)
label.grid(row=i // 3, column=i % 3)

# 主窗口循环显示
window.mainloop()

效果如下:

place()

place() 通过指定控件的绝对位置(或于父控件的相对位置)来布局,非常容易理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import tkinter as tk

# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

label_1 = tk.Label(window, text='label_1', bg='red').place(x=20, y=20)
label_2 = tk.Label(window, text='label_2', bg='yellow').place(x=100, y=20)
label_3 = tk.Label(window, text='label_3', bg='gray').place(x=50, y=80)

# 主窗口循环显示
window.mainloop()

效果如下:

一个简单的示例

下面这段代码添加了几个控件,通过简单的布局,展示了一个常见的登录窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import tkinter as tk
from tkinter import messagebox

# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

# 设置账号密码标签
label_account = tk.Label(window, text='账号:').place(x=50, y=50)
label_password = tk.Label(window, text='密码:').place(x=50, y=100)

# 设置账号密码输入框
entry_account = tk.Entry(window)
entry_account.place(x=100, y=50)
entry_password = tk.Entry(window, show='*') # 显示成密文形式
entry_password.place(x=100, y=100)

# 设置登录按钮
btn_login = tk.Button(window, text="登录")
btn_login.place(x=150, y=150)

# 主窗口循环显示
window.mainloop()

效果如下:

设置控件响应函数

在上面的例子中,我们成功地向窗口中添加了Label, Entry , Button 等组件,但此时我们的控件并没有关联任何函数。当你点击按钮时,得不到任何响应,熟悉 GUI 编程的都知道控件都需要一个响应函数,让我们在点击按钮时得到反馈。

具体实现起来也很简单,我们只需要额外定义一个函数,将控件与这个函数绑定即可。

1
2
3
4
5
6
7
8
from tkinter import messagebox

def onClickLogin():
messagebox.showinfo(title='提示', message='Login')

# 设置登录按钮
btn_login = tk.Button(window, text="登录", command=onClickLogin)
btn_login.place(x=150, y=150)

在上面这段代码中,我们定义了一个函数 onClickLogin,它的功能是弹出一个消息提示框,标题为 提示,内容为 Login;同时,对 btn_login 进行了修改,在初始化时添加了 command=onClickLogin 字段,它的功能也就是将按钮 btn_login 与函数 onClickLogin 绑定。

获取并显示账号密码

学会了添加控件响应函数,那么就让我们在之前例子的基础上添加一个小功能:当你输入账号密码之后,点击登录,弹出你输入的账号密码。毕竟在上面的例子中,我们并没有关注输入了什么内容,也没有对账号密码进行保存。

首先,我们需要知道的是有些控件可以通过传入特定参数直接和一个控件绑定,这种绑定是双向的: 如果该变量发生改变, 与该变量绑定的控件也会随之更新

下面的这段代码中,我们就创建了两个 StringVar 类型的变量,并将 accountpassword 分别与 entry_accountentry_password 进行绑定:

1
2
3
4
5
6
7
# 初始化存放账号密码的变量
account = tk.StringVar()
password = tk.StringVar()

# 设置账号密码输入框
entry_account = tk.Entry(window, textvariable=account)
entry_password = tk.Entry(window, textvariable=password, show='*')

StringVartkinter 中变量类的一个,它保存一个 string 类型变量,默认值为 ""

当然,类似的也有 IntVarDoubleVarBooleanVar,我想你也同样能够理解它的意思。

要得到其保存的变量值, 使用它的 get() 方法即可。

要设置其保存的变量值, 使用它的 set() 方法即可。

1
2
3
4
5
6
7
account = tk.StringVar()

# string -> StringVar
account.set('123456')

# StringVar -> string
str_account = account.get()

同时,我们还需要对 onClickLogin 进行修改,通过 get() 转换为 string 类型,并通过消息框弹出信息。

1
2
3
def onClickLogin():
msg = '账号:' + account.get() + ' 密码:' + password.get()
messagebox.showinfo(title='提示', message=msg)

完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import tkinter as tk
from tkinter import messagebox


def onClickLogin():
msg = '账号:' + account.get() + ' 密码:' + password.get()
messagebox.showinfo(title='提示', message=msg)


# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

# 设置账号密码标签
label_account = tk.Label(window, text='账号:').place(x=50, y=50)
label_password = tk.Label(window, text='密码:').place(x=50, y=100)

# 初始化存放账号密码的变量
account = tk.StringVar()
password = tk.StringVar()

# 设置账号密码输入框
entry_account = tk.Entry(window, textvariable=account)
entry_account.place(x=100, y=50)
entry_password = tk.Entry(window, textvariable=password, show='*') # 显示成密文形式
entry_password.place(x=100, y=100)

# 设置登录按钮
btn_login = tk.Button(window, text="登录", command=onClickLogin)
btn_login.place(x=150, y=150)

# 主窗口循环显示
window.mainloop()

效果如下:

添加更多自定义设置

在上面的示例中,我们并没有过多的关注控件的大小、颜色、字体等信息,但实际上对于大多数的控件,你都可以自定义这些属性。

1
2
3
4
var = tk.StringVar()
var.set('This is a label')
l = tk.Label(window, textvariable=var, bg='green', fg='white', font=('Arial', 12), width=30, height=2)
l.pack()

效果如下:

单选、复选、下拉框

对于单选、复选、下拉框,我想大家都不陌生,在我们填写各种表单、问卷的时候就经常见到,这里我们仍然通过一个简单的示例来展示用法。

添加单选框

单选框要求我们从 n 个选项中选择一个选项,因此我们需要将这 n 的单选框都绑定到一个变量上,正如下面代码中展示的,value 属性用于多个单选框值的区别,我们把 rad_gender_1rad_gender_2 都绑定到了变量 gender,当我们选中了其中一个选项,就会把 value 的值 1 放到变量 gender

1
2
3
4
5
6
7
8
# 选择性别
gender = tk.IntVar()

# 其中当我们选中了其中一个选项,把 value 的值 1 放到变量 gender 中
rad_gender_1 = tk.Radiobutton(window, text='男', variable=gender, value=1)
rad_gender_2 = tk.Radiobutton(window, text='女', variable=gender, value=2)
rad_gender_1.place(x=100, y=50)
rad_gender_2.place(x=150, y=50)

添加下拉框

下拉框可以让我们从多个选项中选择一个选项。在下面的示例中,下拉框 combo_birth_year 会将选择的值传递给绑定的变量 birth_year;另一方面,可以通过设置 value 字段设置待选项。

1
2
3
4
5
6
7
8
9
from tkinter import ttk

# 选择出生年月
birth_year = tk.StringVar()
# 创建下拉框实例
combo_birth_year = ttk.Combobox(window, width=8, textvariable=label_birth_year)
# 设置下拉框选项
combo_birth_year['value'] = [str(i) for i in range(1950, 2021)]
combo_birth_year.place(x=100, y=80)

添加多选框

多选框允许我们从 n 个选项中选择 1 - n 个选项。在下面的示例中,我们创建了一个字典存储不同的爱好,同样创建了 n 个多选框实例,并且将值依次存入 dic_hobby

1
2
3
4
5
6
7
8
# 选择爱好
hobbys = {0: '唱歌', 1: '跳舞', 2: '篮球', 3: '足球', 4: '绘画'}
dic_hobby = {}

for i in range(len(hobbys)):
dic_hobby[i] = tk.BooleanVar()
cbtn_hobby = tk.Checkbutton(window, text=hobbys[i], variable=dic_hobby[i])
cbtn_hobby.place(x=100 + i * 60, y=110)

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox

# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

# 输入姓名
tk.Label(window, text='姓名: ').place(x=20, y=20)
entry_name = tk.Entry(window).place(x=100, y=20)

# 选择性别
tk.Label(window, text='性别: ').place(x=20, y=50)
gender = tk.IntVar()
# 其中当我们选中了其中一个选项,把 value 的值 '男' 放到变量 gender 中
rad_gender_1 = tk.Radiobutton(window, text='男', variable=gender, value=1)
rad_gender_2 = tk.Radiobutton(window, text='女', variable=gender, value=2)
rad_gender_1.place(x=100, y=50)
rad_gender_2.place(x=150, y=50)

# 选择出生年月
tk.Label(window, text='出生年月: ').place(x=20, y=80)
tk.Label(window, text='年 ').place(x=180, y=80)
tk.Label(window, text='月 ').place(x=290, y=80)

birth_year = tk.StringVar()
combo_birth_year = ttk.Combobox(window, width=8, textvariable=birth_year)
combo_birth_year['value'] = [str(i) for i in range(1950, 2021)]
combo_birth_year.place(x=100, y=80)

birth_mon = tk.StringVar()
combo_birth_mon = ttk.Combobox(window, width=8, textvariable=birth_mon)
combo_birth_mon['value'] = [str(i) for i in range(1, 13)]
combo_birth_mon.place(x=210, y=80)

# 选择爱好
tk.Label(window, text='爱好: ').place(x=20, y=110)
hobbys = {0: '唱歌', 1: '跳舞', 2: '篮球', 3: '足球', 4: '绘画'}
dic_hobby = {}

for i in range(len(hobbys)):
dic_hobby[i] = tk.BooleanVar()
cbtn_hobby = tk.Checkbutton(window, text=hobbys[i], variable=dic_hobby[i])
cbtn_hobby.place(x=100 + i * 60, y=110)

# 主窗口循环显示
window.mainloop()

效果如下:

Canvas 画布

Canvas,提供绘图功能,提供的图形组件包括:线形, 圆形, 图片…

类似的,我们使用如下命令创建一个 Canvas 实例,为了明显,我们将背景色设定为黄色

1
cv = tk.Canvas(window, bg='yellow')

下面的例子中,我们绘制了一条直线,从 (0, 50)(80, 80);绘制了一个矩形,它的左上和右下顶点的坐标分别是 (30, 100)(70, 150);最后通过 create_image() 导入了一张图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import tkinter as tk

# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

cv = tk.Canvas(window, bg='yellow')
cv.pack()

# 画线(起点终点)
line = cv.create_line(0, 50, 80, 80)
# 画矩形(对角线坐标)
rect = cv.create_rectangle(30, 100, 70, 150)
# 导入图片
img = tk.PhotoImage(file='bubblesort.gif')
# anchor='nw': 左上角锚定,放在画布(100,100)坐标处
cv.create_image(100, 100, anchor='nw', image=img)

# 主窗口循环显示
window.mainloop()

效果如下:

菜单栏和子窗口

添加菜单栏

菜单功能同样是比较常见的,我们可以在各种软件上发现菜单。在 tkinter 中,同样可以很容易地添加菜单栏。

在下面的代码中,我们首先创建了一个菜单栏 menubar,接着又创建了两个菜单项 menu_filemenu_edit,并通过 add_cascade() 将两个菜单项 FileEdit添加到菜单栏中;然后又在菜单项 File 中加入内容 new,open,save 等字段,这里没有实现具体的功能,你可以自己添加 command 参数以实现响应。最后,还需要设置主窗口的 menu 参数,将 menubar 配置到窗口中。

类似的,你也可以通过设定层次关系实现二级、三级菜单,只需要正确的指定父子 menu 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import tkinter as tk

# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

# 创建菜单栏
menubar = tk.Menu(window)

# 创建菜单项
menu_file = tk.Menu(menubar, tearoff=0)
menu_edit = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label='File', menu=menu_file)
menubar.add_cascade(label='Edit', menu=menu_edit)

# 在菜单项 File 中加入内容
menu_file.add_cascade(label='new')
menu_file.add_cascade(label='open')
menu_file.add_separator() # 添加分割线
menu_file.add_cascade(label='save')

# 在主窗口配置菜单栏
window.config(menu=menubar)

# 主窗口循环显示
window.mainloop()

效果如下:

添加子窗口

很多情况下,一个窗口往往不足以展示我们需要的全部信息,因此这时候我们可以创建子窗口

下面的例子中,我们在前面的基础上为 File 菜单项中的 new 按钮添加了事件函数 onClickNew(),它会创建一个子窗口 sub_window,注意此时创建出来的窗口必须是 Toplevel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import tkinter as tk


def onClickNew():
sub_window = tk.Toplevel(window)
sub_window.title('sub_window')
sub_window.geometry('300x200')
tk.Label(sub_window, text='This is a sub window').pack()


# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

# 创建菜单栏
menubar = tk.Menu(window)

# 创建菜单项
menu_file = tk.Menu(menubar, tearoff=0)
menu_edit = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label='File', menu=menu_file)
menubar.add_cascade(label='Edit', menu=menu_edit)

# 在菜单项 File 中加入内容
menu_file.add_cascade(label='new', command=onClickNew)
menu_file.add_cascade(label='open')
menu_file.add_separator() # 添加分割线
menu_file.add_cascade(label='save')

# 在主窗口配置菜单栏
window.config(menu=menubar)

# 主窗口循环显示
window.mainloop()

效果如下:

文件对话框

下面让我们来实现一个小功能,点击 选择路径 按钮,打开文件对话框,选定路径后列出该路径下的所有文件和文件夹。

让我们一步一步来实现,首先,我们需要做出一个界面,大概想想你见过的文件选择对话框,我相信这并不困难。

1
2
3
4
5
6
7
8
9
10
# 放置文件展示列表
window_list = tk.Text(window, width=60, height=10, state='disabled')
window_list.place(x=30, y=70)

# 存放文件路径
path = tk.StringVar()
# 放置路径选择模块
tk.Label(window, text="目标路径:").place(x=50, y=30)
tk.Entry(window, textvariable=path, width=30).place(x=120, y=30)
tk.Button(window, text="路径选择", command=onClickSelectPath).place(x=350, y=25)

上面的代码我相信已经很熟悉了,我们设计了布局,在 路径选择 按钮上添加了函数 onClickSelectPath()。值得注意的是,我们将 Text 设为禁止,这意味你不能写入任何字段。

现在让我们来看看 onClickSelectPath() 怎么实现,我们可以通过添加 askdirectory() 函数请求目录;然后通过 set() 更新 path 的路径,注意这里 path 是和 Entry 绑定了,因此更新了 path 之后,Entry 中会自动显示该路径。

1
2
3
4
5
from tkinter.filedialog import askdirectory

def onClickSelectPath():
_path = askdirectory()
path.set(_path)

得到了文件路径之后,我们便可以通过 listdir() 获得所有文件。

1
2
# 获得当前路径下的所有文件名
file_lists = os.listdir(file_dir)

接着,我们只需要把获得的文件写入 Text,由于之间我们在创建时将 Text 设为了禁止,因此在写入数据之间,需要将其重置为 normal,等到写入完成之后再 disabled

我们使用 deleteinsert 进行数据的删除和插入,你只需要指定插入的位置内容即可。

1
2
3
4
5
6
7
8
9
10
11
# 在更新列表前恢复写
window_list.config(state='normal')
# 删除原来的数据
window_list.delete(1.0, tk.END)

for file_name in file_lists:
window_list.insert(tk.END, file_name)
window_list.insert(tk.END, '\n')

# 恢复为只读
window_list.config(state='disabled')

完整代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import os
import tkinter as tk
from tkinter.filedialog import askdirectory


def onClickSelectPath():
_path = askdirectory()
path.set(_path)

# 根据路径获取所有专利列表,并展示
file_dir = path.get()
if file_dir:
# 获得当前路径下的所有文件名
file_lists = os.listdir(file_dir)
# 在更新列表前恢复写
window_list.config(state='normal')
# 删除原来的数据
window_list.delete(1.0, tk.END)

for file_name in file_lists:
window_list.insert(tk.END, file_name)
window_list.insert(tk.END, '\n')

# 恢复为只读
window_list.config(state='disabled')


# 创建一个窗口实例
window = tk.Tk()

# 设置窗口标题
window.title('my_window')

# 设置窗口大小和位置(宽度 x 高度 + x偏移 + y偏移)
window.geometry('500x300')

# 放置文件展示列表
window_list = tk.Text(window, width=60, height=10, state='disabled')
window_list.place(x=30, y=70)

# 存放文件路径
path = tk.StringVar()
# 放置路径选择模块
tk.Label(window, text="目标路径:").place(x=50, y=30)
tk.Entry(window, textvariable=path, width=30).place(x=120, y=30)
tk.Button(window, text="路径选择", command=onClickSelectPath).place(x=350, y=25)

# 主窗口循环显示
window.mainloop()

效果如下:

打包为 exe

写完了程序之后,我们不可能直接丢给别人一个 py 文件,还要将其打包为 exe

目前比较常见的打包 exe 方法都是通过 pyinstaller 来实现的,使用安装命令进行安装:

1
pip install pyinstaller

pyinstaller 打包 exe

进入命令行界面,进入当前 .py 所在的目录,也就是你要打包的文件,(当然简单的方式是按住 shift 然后右键,进入命令行界面)

然后输入如下命令:

1
pyinstaller -F test.py

另外你也可以指定 pyinstaller 的参数:

1
2
3
4
5
6
# 打包exe
pyinstaller -F py_word.py
# 不带控制台的打包
pyinstaller -F -w py_word.py
# 打包指定exe图标打包
pyinstaller -F -w -i chengzi.ico py_word.py

参考资料


本文作者: EmoryHuang
本文链接: https://emoryhuang.cn/blog/1384140617.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明来自EmoryHuang