引言  - 从"HelloWorld"开始

  Makefile 是Linux C 程序开发最重要的基本功. 代表着整个项目编译和最终生成过程.本文重点是带大家了解真实项目中那些简易的Makefile规则构建.

本文参照资料

   GNU make   -  https://www.gnu.org/software/make/manual/make.html  

   跟我一起写Makefile  - http://wiki.ubuntu.org.cn/%E8%B7%9F%E6%88%91%E4%B8%80%E8%B5%B7%E5%86%99Makefile:%E6%A6%82%E8%BF%B0 

   入门基础Makefile概述  - https://github.com/loverszhaokai/GNUMakeManual_CN

推荐需要简单看看上面资料. 特别是第三个入门教程, 了解基础make语法.  看完后那我们扩展之路开始了, 先hello world 讲起. 素材 mian.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h> #define ALEN(arr) (sizeof(arr)/sizeof(*arr)) /*
* 简单的demo测试
*/
int main(int argc, char * argv[]) {
int i;
const char * strs[] = {
"走着走着,就散了,回忆都淡了",
"看着看着,就累了,星光也暗了;",
"听着听着,就醒了,开始埋怨了;",
"回头发现,你不见了,突然我乱了。",
}; srand((unsigned)time(NULL));
for(;;) {
/*
* \e[ 或 \033[ 是 CSI,用来操作屏幕的。
* \e[K 表示从光标当前位置起删除到 EOL (行尾)
* \e[NX 表示将光标往X方向移动N,X = A(上) / B(下) / C(左) / D(右),\e[1A 就是把光标向上移动1行
*/
printf("\033[1A\033[K"); //先回到上一行, 然后清除这一行 // 随机输出一段话
i = rand()%ALEN(strs);
puts(strs[i]); sleep();
} return ;
}

编译上面程序的第一个Makefile 文件内容如下

main.out:main.c
gcc -g -Wall -o $@ $^

执行过程就是通过shell执行make, 我们简单翻译一下上面写法的含义.

  目标 main.out  依赖 main.c ;  main.c 已经存在(因为是存在的文件) 那就执行规则 (gcc -g -Wall -o $@ $^).

  其中 $@ 表示所有目标, $^表示所有依赖. 

是不是很简单.当然上面Makefile还存在一些潜规则.

  所有执行规则都是以\t开始; 第一个目标就是make过程唯一执行的起点;

再讲之前我们再扯一点gcc 相关的积累知识. 否则写Makefile都是无米之炊.

# 中间插入一段关于gcc 的前戏
gcc –E –o main.i mian.c    # -E是预处理展开宏,生成详细c文件, -o是输出
gcc –S –o main.s main.i    # -S 是编译阶段, 将c文件生成汇编语言
gcc –c –o main.o main.s    # -c 是汇编阶段, 生成机器码
gcc –o main.exe main.o     # 链接阶段, -o 生成目标执行程序 gcc –g      # 编译中加入调试信息, 方便gdb调试, 还有-ggdb3 支持宏调试等
gcc –Wall    # 输出所有警告信息
gcc –O2 # 开启调优, O2等级调优 gcc –I(i大写) # 导入头文件目录,方便 *.h文件查找
gcc –L(l 大写)   # 导入库文件目录,方便 *.so和*.a文件查找
gcc –l(l 小写) # 导入库文件, 例如-lpthread, 相当于依次查找导入 libpthread.so/libpthread.a 文件
gcc –static –l(l 小写) # 指定只查找 静态库 lib*.a 文件, linux约定库文件都是 lib开头 ar rc libheoo.a hello.o world.o # 将*.o 文件打包成 libheoo.a 静态库
gcc –fPIC –shared –o libheoo.so hello.o world.o # 将*.o 文件打包成 libheoo.so 动态库

到这里储备方面的讲完毕了.   --<-<-<@

前言  -  介绍一下实际例子中语法套路

  首先升级一下上面Makefile文件, 如下(如果你复制没法执行, 请检查规则开头字符是\t)

# 构建全局编译操作宏
CC = gcc
CFLAGS = -g -Wall -O2
RUNE = $(CC) $(CFLAGS) -o $@ $^
RUNO = $(CC) -o $@ $< # 构建伪命令
.PHONY:all clean cleanall # 第一个标签, 是make的开始
all:main.out main.out:main.c
$(RUNE) # 清除操作
clean:
-rm -rf *.i *.s *.o *~
cleanall:clean
   -rm -rf *.out *.out *.a *.so

我们先说一下Makefile中变量的使用, 就是上面 "="那种基础语法说明.

关于Makefile 变量总结如下

关于上面变量的使用这里做一个总结. 

a. = 声明变量
加入存在下面场景

CC = cc

CC = gcc 那么make的时候, $(CC) 就是 gcc, 会全局替换.
对于 = 声明的可以认为是一个全局递归替换宏. b. := 声明变量 …
srcdir := ./coroutine
tardir := ./Debug

上面就是一般语言中普通变量. c. ?= 声明变量 Foo ?= bar 上面意思是 $(foo)不存在, 那就将 bar 给它. 等同于
ifeq ($(origin FOO), undefined)
FOO = bar
endif d. += 声明变量 objects = main.o foo.o bar.o utils.o
objects += another.o
等同于 objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o

趁着热度举个例子, 先不解惑.

CC = cc
FOO := foo
BAR ?= bar
HEO := heo all :
echo $(CC)
echo $(FOO)
echo $(BAR)
echo $(HEO) HEO += world
FOO := FOO
CC = gcc

执行结果如下, 如下图 . 通过Demo外加上下面运行结果图, 应该会有收获.

通过上面我们可以发现 := 和 = 声明的变量都是最终全局替换之后的结果. 他们二者细微差别, 我还是通过例子来说吧.

一切都在不言中, 那么关于Makefile变量中语法讲解完毕. 顺带说一些小细节吧,

  1). Makefile 中 一切从简单开始, 能用 = 就不要用 :=

  2). 变量具备全部作用域 , 推荐全部用大写命名

  3). 多查最开始我推荐的资料

接着变量往后讲,继续分析其它例子

上面 .PHONY 是 Makefile中伪命令. 默认套路写法. 定义命令名称, 可以通过 make 命令名称调用.

其中 all 是Makefile第一个运行目标,  从它入口. clean , cleanall 伪命令 通过 make clean ; make cleanall 执行.

主要是清除生成的中间文件. 希望你能明白, 自己演示一下, 是不是这样的.

这里我们开始一个新的例子了. 具体参照

  C协程库的编译文件  https://github.com/wangzhione/scoroutine/blob/master/Makefile

# 全局替换变量
CC = gcc
CFLAGS = -g -Wall -O2
RUNE = $(CC) $(CFLAGS) -o $@ $^ # 声明路径变量
SRC_PATH := ./coroutine
TAR_PATH := ./Debug # 构建伪命令
.PHONY:all clean cleanall # 第一个标签, 是make的开始
all:$(TAR_PATH)/main.out $(TAR_PATH)/main.out:main.o scoroutine.o
$(CC) $(CFLAGS) -o $@ $(addprefix $(TAR_PATH)/, $^ ) $(TAR_PATH):
mkdir $@ %.o:$(SRC_PATH)/%.c | $(TAR_PATH)
$(CC) $(CFLAGS) -c -o $(TAR_PATH)/$@ $< # 清除操作
clean:
  -rm -rf $(TAR_PATH)/*.i $(TAR_PATH)/*.s $(TAR_PATH)/*.o $(TAR_PATH)/*~
cleanall:clean
  -rm -rf $(TAR_PATH)

从头开始分析它的具体含义.

1) 开头全局变量定义部分, 个人习惯问题其实也可以用 := . 最终得到 RUNE = gcc -g -Wall -O2 -o $@ $^ .

2) 路径声明部分, 用 := 声明, 支持中间拼接. 用=也可以, 都是条条大路同罗马, 自己多检查一下. 以后我可能全部用 = 声明全局递归的字面变量声明.

3) .PHONY 声明了 3个伪命令. 不会立即执行的命令, 依赖 make 命令名称 主动调用

4) all 依赖 于 $(TAR_PATH)/main.out 就是依赖于 ./coroutine/main.out. 刚好下面存在

$(TAR_PATH)/main.out:main.o scoroutine.o
$(CC) $(CFLAGS) -o $@ $(addprefix $(TAR_PATH)/, $^ )

这条规则. 其中又依赖于 main.o 和 scoroutine.o 目标. 那么二者也会做新的目标, 就这样递归的找下去.
后面找到了 %.o, Makefile中%是匹配符, 例如 main.o % 就相当于 main部分.
其中addprefix 是GNU make内置的函数的其中一个, 需要用到的时候多查文档就行了.

为每一个可以分割的子单元上加上一个前缀, 这个前缀就是它的第一个参数.

5) 对于下面这段很实用, 通配符 + | 生成必要文件的语法

%.o:$(SRC_PATH)/%.c | $(TAR_PATH)
$(CC) $(CFLAGS) -c -o $(TAR_PATH)/$@ $<

以上是一个通用匹配规则, %.o 目标依赖于 ..../%.c 具体文件. 后面 | 跟的也是一个依赖目标. 这个目标只会在第一次不存在的时候才会被构建.

更加详细的说明可以参照第一个参照资料 "4.3 Types of Prerequisites" 部分.  这个语法用的很多, 用于构建一次生成所需的目录信息.

6) 最后就是剩余clean, cleanall伪命令. 定义清除中间文件等.

是不是想骂die, 但是上面那些都自行捣鼓了一遍, 基本就越过Makefile初级部分, 能够写出能看的编译文件O(∩_∩)O哈哈~

正文  - 来个小框架Makefile试试水

  先找一个特别老的, 很水的一个Makefile 试试. 具体参照

  一个控制台小项目编译文件  https://github.com/wangzhione/sconsole_project/blob/master/linux_sc_template/Makefile

C = gcc
DEBUG = -g -Wall -D_DEBUG
#指定pthread线程库
LIB = -lpthread -lm
#指定一些目录
DIR = -I./module/schead/include -I./module/struct/include
#具体运行函数
RUN = $(CC) $(DEBUG) -o $@ $^ $(LIB) $(DIR)
RUNO = $(CC) $(DEBUG) -c -o $@ $^ $(DIR) # 主要生成的产品
all:test_cjson_write.out test_csjon.out test_csv.out test_json_read.out test_log.out\
test_scconf.out test_tstring.out #挨个生产的产品
test_cjson_write.out:test_cjson_write.o schead.o sclog.o tstring.o cjson.o
$(RUN)
test_csjon.out:test_csjon.o schead.o sclog.o tstring.o cjson.o
$(RUN)
test_csv.out:test_csv.o schead.o sclog.o sccsv.o tstring.o
$(RUN)
test_json_read.out:test_json_read.o schead.o sclog.o sccsv.o tstring.o cjson.o
$(RUN)
test_log.out:test_log.o schead.o sclog.o
$(RUN)
test_scconf.out:test_scconf.o schead.o scconf.o tree.o tstring.o sclog.o
$(RUN)
test_tstring.out:test_tstring.o tstring.o sclog.o schead.o
$(RUN) #产品主要的待链接文件
test_cjson_write.o:./main/test_cjson_write.c
$(RUNO)
test_csjon.o:./main/test_csjon.c
$(RUNO)
test_csv.o:./main/test_csv.c
$(RUNO)
test_json_read.o:./main/test_json_read.c
$(RUNO)
test_log.o:./main/test_log.c
$(RUNO) -std=c99
test_scconf.o:./main/test_scconf.c
$(RUNO)
test_tstring.o:./main/test_tstring.c
$(RUNO) #工具集机械码,待别人链接
schead.o:./module/schead/schead.c
$(RUNO)
sclog.o:./module/schead/sclog.c
$(RUNO)
sccsv.o:./module/schead/sccsv.c
$(RUNO)
tstring.o:./module/struct/tstring.c
$(RUNO)
cjson.o:./module/schead/cjson.c
$(RUNO)
scconf.o:./module/schead/scconf.c
$(RUNO)
tree.o:./module/struct/tree.c
$(RUNO) #删除命令
clean:
rm -rf *.i *.s *.o *.out __* log ; ls -hl
.PHONY:clean

上面那些注释已经表达了一切了吧, 确实好水. 但是特别适合练手, 每一个生成目标都有规则对应. 费力但是最直接. 实在没有没有好讲的, 扯一点

1) GNU make 指定的编译文件是 makefile 或 Makefile. 推荐用Makefile, 是一个传统吧. 因为C项目都是小写, 用大写开头以作区分.

2) Makefile 中 同样以 \ 来起到一整行的效果

3) 其它目标, 依赖, 规则.只要存在那么Makefile就可以自动推导. 当然它依赖文件创建时间戳, 只有它变化了Makefile才会重新生成目标.

Makefile点心结束了. 以上就是make使用本质, 生成什么, 需要什么, 执行什么. 推荐练练手, 手冷写不了代码.

最后来点水果

  simplec c的简易级别框架  https://github.com/wangzhione/simplec/blob/master/Makefile

##################################################################################################
# .前期编译辅助参数支持 #
##################################################################################################
SRC_PATH ?= ./simplec
MAIN_DIR ?= main
SCHEAD_DIR ?= module/schead
SERVICE_DIR ?= module/service
STRUCT_DIR ?= module/struct
TEST_DIR ?= test
TAR_PATH ?= ./Output
BUILD_DIR ?= obj # 指定一些目录
DIR = -I$(SRC_PATH)/$(SCHEAD_DIR)/include -I$(SRC_PATH)/$(SERVICE_DIR)/include \
-I$(SRC_PATH)/$(STRUCT_DIR)/include # 全局替换变量
CC = gcc
LIB = -lpthread -lm
CFLAGS = -g -Wall -O2 -std=gnu99 # 运行指令信息
define NOW_RUNO
$(notdir $(basename $())).o : $() | $$(TAR_PATH)
$$(CC) $$(CFLAGS) $$(DIR) -c -o $$(TAR_PATH)/$$(BUILD_DIR)/$$@ $$<
endef # 单元测试使用, 生成指定主函数的运行程序
RUN_TEST = $(CC) $(CFLAGS) $(DIR) --entry=$(basename $@) -nostartfiles -o \
$(TAR_PATH)/$(TEST_DIR)/$@ $(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v)) # 产生具体的单元测试程序
define TEST_RUN
$() : $$(notdir $$(basename $())).o libschead.a $() | $$(TAR_PATH)
$$(RUN_TEST) $(LIB)
endef ##################################################################################################
# .具体的产品生产 #
##################################################################################################
.PHONY:all clean cleanall all : main.out\
$(foreach v, $(wildcard $(SRC_PATH)/$(TEST_DIR)/*.c), $(notdir $(basename $(v))).out) # 主运行程序main
main.out:main.o simplec.o libschead.a libstruct.a test_sctimeutil.o
$(CC) $(CFLAGS) $(DIR) -o $(TAR_PATH)/$@ $(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v)) $(LIB) # !!!!! - 生成具体的单元测试程序 - 依赖个人维护 - !!!!!
$(eval $(call TEST_RUN, test_array.out, array.o))
$(eval $(call TEST_RUN, test_atom_rwlock.out))
$(eval $(call TEST_RUN, test_cjson.out, tstr.o))
$(eval $(call TEST_RUN, test_cjson_write.out, tstr.o))
$(eval $(call TEST_RUN, test_csv.out, tstr.o))
$(eval $(call TEST_RUN, test_json_read.out, tstr.o))
$(eval $(call TEST_RUN, test_log.out))
$(eval $(call TEST_RUN, test_scconf.out, tstr.o tree.o))
$(eval $(call TEST_RUN, test_scoroutine.out, scoroutine.o))
$(eval $(call TEST_RUN, test_scpthread.out, scpthread.o scalloc.o))
$(eval $(call TEST_RUN, test_sctimer.out, sctimer.o scalloc.o))
$(eval $(call TEST_RUN, test_sctimeutil.out))
$(eval $(call TEST_RUN, test_tstring.out, tstr.o))
$(eval $(call TEST_RUN, test_xlsmtojson.out, tstr.o)) ##################################################################################################
# 2.先产生所需要的所有机器码文件 #
################################################################################################## # 循环产生 - 所有 - 链接文件 *.o
SRC_CS = $(wildcard\
$(SRC_PATH)/$(MAIN_DIR)/*.c\
$(SRC_PATH)/$(TEST_DIR)/*.c\
$(SRC_PATH)/$(SCHEAD_DIR)/*.c\
$(SRC_PATH)/$(SERVICE_DIR)/*.c\
$(SRC_PATH)/$(STRUCT_DIR)/*.c\
)
$(foreach v, $(SRC_CS), $(eval $(call NOW_RUNO, $(v)))) # 生产 -相关- 静态库
libschead.a : $(foreach v, $(wildcard $(SRC_PATH)/$(SCHEAD_DIR)/*.c), $(notdir $(basename $(v))).o)
ar cr $(TAR_PATH)/$(BUILD_DIR)/$@ $(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v))
libstruct.a : $(foreach v, $(wildcard $(SRC_PATH)/$(STRUCT_DIR)/*.c), $(notdir $(basename $(v))).o)
ar cr $(TAR_PATH)/$(BUILD_DIR)/$@ $(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v)) ##################################################################################################
# 3.程序的收尾工作,清除,目录构建 #
##################################################################################################
$(TAR_PATH):
-mkdir -p $@/$(BUILD_DIR)
-mkdir -p $@/test/config
-cp -r $(SRC_PATH)/test/config $@/test # 清除操作
clean :
-rm -rf $(TAR_PATH)/$(BUILD_DIR)/* cleanall :
-rm -rf $(TAR_PATH)

具体可以参照simplec 项目查看, 我们抽一部分重点讲解

define NOW_RUNO
$(notdir $(basename $())).o : $() | $$(TAR_PATH)
$$(CC) $$(CFLAGS) $$(DIR) -c -o $$(TAR_PATH)/$$(BUILD_DIR)/$$@ $$<
endef

上面定义了一个语句块 NOW_RUNO. 其中语句块中除了要接收的参数可以用$(1), $(2) ..., 其它都是两个$$开头, 否则就被替换了. 使用方法就是

$(eval $(call NOW_RUNO, $(v)))

通过$eval(), $(call ) 这种套路调用. call NOW_RUNO, 后面添加都是 NOW_RUNO语句块的函数了.

这里说一个Makefile处理的潜在小问题, 当你传入参数是依赖项时候, 如果不是直接通过唯一一个参数传入进去,

那么解析的是当成多个依赖项处理.所以上面只有 $(1)做依赖项.

Makefile中 foreach语法也很好用等同于shell语法传参方式.

$(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v))
将第二个$^通过空格分隔成单个的v代替, 被替换为第三个中一部分. $(foreach ...)执行完毕最终返回一个拼接好的串

在简单补充几个函数说明 例如

$(1) => $$(notdir $$(basename $())).o <=> ./simplec/main/main.c => main.o

其中 nodir函数得到文件名, basename函数得到文件名不包过.和.后面部分.
wildcard 函数是得到指定匹配规则下的文件全路径拼接.
最后面 -rm 那些, 加了前缀 - 是为了当Makefile执行到这如果运行出错, 不停止继续前行.
通过上面Makefile最终跑起来后, 会生成一个Output目录, 再在内部生成 obj, test, ...
还是很有学习价值的. 有兴趣的可以试试.
希望通过上面讲解, 能够使你以后阅读其它更高级项目的编译文件不那么生疏. (* ̄(エ) ̄)

后记  -  突然想起了什么, 笑了笑 我自己 ...

  伽罗  -  http://music.163.com/#/artist/desc?id=21309

Makefile 跟着走快点的更多相关文章

  1. 跟着Sedgewick学算法(week 1 UnionFind)

    发现笔记转过来,没有图的~~~~~~~~~~~悲剧,给出共享笔记链接 https://www.evernote.com/pub/yanbinliu/algorithm 很久之前就在coursera看到 ...

  2. 一个五年 Android 开发者百度、阿里、聚美、映客的面试心经

    花絮 也许会有人感叹某些人的运气比较好,但是他们不曾知道对方吃过多少苦,受过多少委屈.某些时候就是需要我们用心去发现突破点,然后顺势而上,抓住机遇,那么你将会走向另外一条大道,成就另外一个全新的自我. ...

  3. 萝卜德森的sublime笔记中文翻译版

    我已经使用subliem编辑器版本2接近2个月了,并且我在其中找到了一堆有用的技巧.我发觉应该写下这些技巧,为那些对此感兴趣的人们.我会尽力的详细描述,那些看起来像魔法一样的东西,因为很多非常“酷”的 ...

  4. Java和.net对比分析

    .Net和Java是国内市场占有率最高的两门技术,对于准备学习编程语言的初学者来说,.Net和Java是初学者首先考虑的两门技术,因此很多人一遍遍的问“学.Net还是学Java”,社区中也每天都有“. ...

  5. 如何进入百度、阿里,一个6年Android老司机的面经

    花絮 也许会有人感叹某些人的运气比较好,但是他们不曾知道对方吃过多少苦,受过多少委屈.某些时候就是需要我们用心去发现突破点,然后顺势而上,抓住机遇,那么你将会走向另外一条大道,成就另外一个全新的自我. ...

  6. [LeetCode] Remove Nth Node From End of List 移除链表倒数第N个节点

    Given a linked list, remove the nth node from the end of list and return its head. For example, Give ...

  7. 熟悉HTML CSS布局模型

    HTML最难的地方来了!这个我反复了很多遍, 包括现在写博客, 也对我自己算是一种温习, 我这块怕是没办法写的很好懂, 因为我自己还不能把我学到的准确通俗易懂的表达出来, 给自己记个笔记, 以后再来一 ...

  8. 菜鸟的Python学习之路(流水账)

    揭开Python的面纱 开始是因为别人说Python简单才开始学的,然后那段时间刚考完研,也没什么事,就多少瞅了瞅,然后发现语法的确简单很多,或者说简洁更合适. 当时看的是简明Python教程,没用多 ...

  9. CSS补充之--页面布局、js补充,dom补充

    CSS补充之--页面布局 主站一:(下面是一个大致的模板) <div class="pg-header"> <div style="width: 120 ...

随机推荐

  1. P1315 观光公交

    题目描述 风景迷人的小城Y 市,拥有n 个美丽的景点.由于慕名而来的游客越来越多,Y 市特意安排了一辆观光公交车,为游客提供更便捷的交通服务.观光公交车在第 0 分钟出现在 1号景点,随后依次前往 2 ...

  2. 【Ubuntu】编写一个c语言代码

    安装 sudo apt-get  build-depgcc coding:http://www.cnblogs.com/zero1665/archive/2009/11/03/1595510.html ...

  3. BZOJ4355:Play with sequence——题解

    https://www.lydsy.com/JudgeOnline/problem.php?id=4355 维护一个长度为N的序列a,现在有三种操作: 1)给出参数U,V,C,将a[U],a[U+1] ...

  4. cdh版本的sqoop安装以及配置

    sqoop安装需要提前安装好sqoop依赖:hadoop .hive.hbase.zookeeper hadoop安装步骤请访问:http://www.cnblogs.com/xningge/arti ...

  5. eclipse中支持python

    1. 启动eclipse,help-> Install New Software; 2. 点击add 3. 设置Repository name: pydev Location: http://p ...

  6. bzoj1177 [Apio2009]Oil 二维前缀最大值,和

    [Apio2009]Oil Time Limit: 15 Sec  Memory Limit: 162 MBSubmit: 2300  Solved: 932[Submit][Status][Disc ...

  7. git版本回退与撤销操作

    场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout -- file. 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步, ...

  8. python---异步IO(asyncio)协程

    简单了解 在py3中内置了asyncio模块.其编程模型就是一个消息循环. 模块查看: from .base_events import * from .coroutines import * #协程 ...

  9. Activiti工作流——流程表数据转化

    任务流程部署:  启动流程实例: 请假人完成请假申请: 部门经理完成审批: 总经理审批完成:

  10. 页面自适应<meta name="viewport">标签设置

    viewport: 它在页面中设置,是应对手机模式访问网站.网页对屏幕而做的一些设置.通常手机浏览器打开页面后,会把页面放在一个虚拟的“窗 口”–这个比窗口大,也就是你常发现页面可以进行拖动.放大放小 ...