0%

Linux

博主之前一直用的是 apache,随着网站负荷量增高,感觉 apache 稍微有点力不从心了。随着 nginx 越来越流行,而且其功能强大,博主准备采用 nginx 作为自己的服务器啦。 每当到了环境配置的时候,博主便会去网上各种搜集资料,然而感觉他们讲的都条理不一,有的地方并不符合自己的配置习惯,所以博主习惯自己配置的同时把配置过程总结一下,方便自己,也方便大家。 好,接下来我们就开始我们的环境配置之旅吧。

Nginx

1.更新源

1
sudo apt-get update

2.安装 nginx

1
sudo apt-get install nginx

3.检查是否安装成功

输入 localhost 或者远程地址,若出现 Welcome To Nginx 则证明安装成功。 20150911144903 如果没有看到,可以运行

1
sudo killall apache2

杀掉 apache 进程,因为可能 80 端口被占用了。

4.更改运行目录

默认的 nginx 目录是/usr/share/nginx/html,我们将其修改为/var/www,当然可以根据个人习惯灵活更改。 修改/etc/nginx/sites-available/default 文件

1
root /usr/share/nginx/html;

更改为

1
root /var/www

再将

1
index index.html index.htm;

更改为

1
index index.html index.php index.htm;

重启 nginx

1
sudo service nginx restart

我们在/var/www 目录下新建 index.html 文件,写入一些测试文字。 重新访问 localhost 或者远程地址,可以看到刚才设置的 index.html 文件中的内容,说明目录已经更改成功了。

PHP

安装 PHP 以及相关扩展。

1
sudo apt-get install php5 php5-cgi php5-mysql php5-curl php5-gd php5-idn php-pear php5-imagick php5-imap php5-mcrypt php5-memcache php5-mhash php5-ming php5-pspell php5-recode php5-snmp php5-tidy php5-xmlrpc php5-sqlite php5-xsl

执行上述指令即可。 此时有的小伙伴想要测试运行 PHP 文件了,然而很悲剧地告诉你是不可以的,因为你还没有配置 fastcgi,继续往下看。

MySQL

执行如下命令,安装 MySQL 服务端和客户端。

1
sudo apt-get install mysql-server mysql-client

在安装时可能提示你输入 root 用户的密码,设置即可。

phpMyAdmin

执行如下命令,安装 phpMyAdmin。

1
sudo apt-get install phpmyadmin

创建软连接

1
sudo ln -s /usr/share/phpmyadmin/ /var/www/phpmyadmin

我们将创建一个根目录为/var/www,链接名为 phpmyadmin 的文件,指向/usr/share/phpmyadmin

spawn-fcgi

1.安装 fastcgi

1
sudo apt-get install spawn-fcgi

2.配置 fastcgi

修改 /etc/nginx/fastcgi_params 文件,增加下面一行

1
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

修改/etc/php5/cgi/php.ini 文件,将下面一行取消注释

1
cgi.fix_pathinfo=1;

这样 php-cgi 方能正常使用 SCRIPT_FILENAME 这个变量 修改/etc/nginx/sites-available/default 文件,我们之前修改的目录地址是/var/www,将

1
2
3
4
5
6
7
8
9
10
11
#location ~ \.php$ {
# fastcgi_split_path_info ^(.+\.php)(/.+)$;
# # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
#
# # With php5-cgi alone:
# fastcgi_pass 127.0.0.1:9000;
# # With php5-fpm:
# fastcgi_pass unix:/var/run/php5-fpm.sock;
# fastcgi_index index.php;
# include fastcgi_params;
#}

修改为

1
2
3
4
5
6
7
8
9
10
11
12
location ~ \.php$ {
# fastcgi_split_path_info ^(.+\.php)(/.+)$;
# # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
#
# # With php5-cgi alone:
fastcgi_pass 127.0.0.1:9000;
# # With php5-fpm:
# fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /var/www$fastcgi_script_name;
include fastcgi_params;
}

3.开启 fastcgi 进程

1
sudo /usr/bin/spawn-fcgi -a 127.0.0.1 -p 9000 -C 5 -u www-data -g www-data -f /usr/bin/php5-cgi -P /var/run/fastcgi-php.pid

参数含义如下

-f 指定调用 FastCGI 的进程的执行程序位置,根据系统上所装的 PHP 的情况具体设置 -a 绑定到地址 addr -p 绑定到端口 port -s 绑定到 unix socket 的路径 path -C 指定产生的 FastCGI 的进程数,默认为 5(仅用于 PHP) -P 指定产生的进程的 PID 文件路径 * -u 和-g FastCGI 使用什么身份(-u 用户 -g 用户组)运行,Ubuntu 下可以使用 www-data,其他的根据情况配置,如 nobody、apache 等现在可以在 web 根目录下放个探针或 php 文件测试一下了

运行结果类似如下

1
spawn-fcgi: child spawned successfully: PID: 11775

4.设置开机启动 fastcgi

修改/etc/rc.local 文件,添加下面一行

1
/usr/bin/spawn-fcgi -a 127.0.0.1 -p 9000 -C 5 -u www-data -g www-data -f /usr/bin/php5-cgi -P /var/run/fastcgi-php.pid

修改完之后,重启 nginx

1
sudo service nginx restart

我们可以在/var/www 目录下新建 index.php 文件测试运行,发现已经可以运行 PHP 文件了。

测试运行

所有配置已经完毕,现在我们输入 localhost 和 localhost/phpmyadmin 便可以轻松加愉快地访问了。 如有问题,欢迎交流。

Python

2022 年最新 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

如下为原文。

大家好,本次为大家带来的是抓取爱问知识人的问题并将问题和答案保存到数据库的方法,涉及的内容包括:

  • Urllib 的用法及异常处理
  • Beautiful Soup 的简单应用
  • MySQLdb 的基础用法
  • 正则表达式的简单应用

环境配置

在这之前,我们需要先配置一下环境,我的 Python 的版本为 2.7,需要额外安装的库有两个,一个是 Beautiful Soup,一个是 MySQLdb,在这里附上两个库的下载地址, Beautiful Soup MySQLdb 大家可以下载之后通过如下命令安装

1
python setup.py install

环境配置好之后,我们便可以开心地撸爬虫了

框架思路

首先我们随便找一个分类地址,外语学习 - 爱问知识人,打开之后可以看到一系列的问题列表。 我们在这个页面需要获取的东西有: 总的页码数,每一页的所有问题链接。 接下来我们需要遍历所有的问题,来抓取每一个详情页面,提取问题,问题内容,回答者,回答时间,回答内容。 最后,我们需要把这些内容存储到数据库中。

要点简析

其实大部分内容相信大家会了前面的内容,这里的爬虫思路已经融汇贯通了,这里就说一下一些扩展的功能

1.日志输出

日志输出,我们要输出时间和爬取的状态,比如像下面这样:

[2015-08-10 03:05:20] 113011 号问题存在其他答案 我个人认为应该是樱桃沟很美的 [2015-08-10 03:05:20] 保存到数据库,此问题的 ID 为 113011 [2015-08-10 03:05:20] 当前爬取第 2 的内容,发现一个问题 百度有一个地方,花儿带着芳香,水儿流淌奔腾是什么意思 多多帮忙哦 回答数量 1 [2015-08-10 03:05:19] 保存到数据库,此问题的 ID 为 113010

所以,我们需要引入时间函数,然后写一个获取当前时间的函数

1
2
3
4
5
6
7
8
9
import time

#获取当前时间
def getCurrentTime(self):
return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

#获取当前时间
def getCurrentDate(self):
return time.strftime('%Y-%m-%d',time.localtime(time.time()))

以上分别是获取带具体时间和获取日期的函数,在输出时,我们可以在输出语句的前面调用这函数即可。 然后我们需要将缓冲区设置输出到 log 中,在程序的最前面加上这两句即可

1
2
f_handler=open('out.log', 'w')
sys.stdout=f_handler

这样,所有的 print 语句输出的内容就会保存到 out.log 文件中了。

2.页码保存

爬虫爬取过程中可能出现各种各样的错误,这样会导致爬虫的中断,如果我们重新运行爬虫,那么就会导致爬虫从头开始运行了,这样显然是不合理的。所以,我们需要把当前爬取的页面保存下来,比如可以保存到文本中,假如爬虫中断了,重新运行爬虫,读取文本文件的内容,接着爬取即可。 大家可以稍微参考一下函数的实现:

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
#主函数
def main(self):
f_handler=open('out.log', 'w')
sys.stdout=f_handler
page = open('page.txt', 'r')
content = page.readline()
start_page = int(content.strip()) - 1
page.close()
print self.getCurrentTime(),"开始页码",start_page
print self.getCurrentTime(),"爬虫正在启动,开始爬取爱问知识人问题"
self.total_num = self.getTotalPageNum()
print self.getCurrentTime(),"获取到目录页面个数",self.total_num,"个"
if not start_page:
start_page = self.total_num
for x in range(1,start_page):
print self.getCurrentTime(),"正在抓取第",start_page-x+1,"个页面"
try:
self.getQuestions(start_page-x+1)
except urllib2.URLError, e:
if hasattr(e, "reason"):
print self.getCurrentTime(),"某总页面内抓取或提取失败,错误原因", e.reason
except Exception,e:
print self.getCurrentTime(),"某总页面内抓取或提取失败,错误原因:",e
if start_page-x+1 < start_page:
f=open('page.txt','w')
f.write(str(start_page-x+1))
print self.getCurrentTime(),"写入新页码",start_page-x+1
f.close()

这样,不管我们爬虫中途遇到什么错误,妈妈也不会担心了

3.页面处理

页面处理过程中,我们可能遇到各种各样奇葩的 HTML 代码,和上一节一样,我们沿用一个页面处理类即可。

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
import re

#处理页面标签类
class Tool:

#将超链接广告剔除
removeADLink = re.compile('<div class="link_layer.*?</div>')
#去除img标签,1-7位空格,&nbsp;
removeImg = re.compile('<img.*?>| {1,7}|&nbsp;')
#删除超链接标签
removeAddr = re.compile('<a.*?>|</a>')
#把换行的标签换为\n
replaceLine = re.compile('<tr>|<div>|</div>|</p>')
#将表格制表<td>替换为\t
replaceTD= re.compile('<td>')
#将换行符或双换行符替换为\n
replaceBR = re.compile('<br><br>|<br>')
#将其余标签剔除
removeExtraTag = re.compile('<.*?>')
#将多行空行删除
removeNoneLine = re.compile('\n+')

def replace(self,x):
x = re.sub(self.removeADLink,"",x)
x = re.sub(self.removeImg,"",x)
x = re.sub(self.removeAddr,"",x)
x = re.sub(self.replaceLine,"\n",x)
x = re.sub(self.replaceTD,"\t",x)
x = re.sub(self.replaceBR,"\n",x)
x = re.sub(self.removeExtraTag,"",x)
x = re.sub(self.removeNoneLine,"\n",x)
#strip()将前后多余内容删除
return x.strip()

我们可以用一段含有 HTML 代码的文字,经过调用 replace 方法之后,各种冗余的 HTML 代码就会处理好了。 比如我们这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
<article class="article-content">
<h2>前言</h2>
<p>最近发现MySQL服务隔三差五就会挂掉,导致我的网站和爬虫都无法正常运作。自己的网站是基于MySQL,在做爬虫存取一些资料的时候也是基于MySQL,数据量一大了,MySQL它就有点受不了了,时不时会崩掉,虽然我自己有网站监控和邮件通知,但是好多时候还是需要我来手动连接我的服务器重新启动一下我的MySQL,这样简直太不友好了,所以,我就觉定自己写个脚本,定时监控它,如果发现它挂掉了就重启它。</p>
<p>好了,闲言碎语不多讲,开始我们的配置之旅。</p>
<p>运行环境:<strong>Ubuntu Linux 14.04</strong></p>
<h2>编写Shell脚本</h2>
<p>首先,我们要编写一个shell脚本,脚本主要执行的逻辑如下:</p>
<p>显示mysqld进程状态,如果判断进程未在运行,那么输出日志到文件,然后启动mysql服务,如果进程在运行,那么不执行任何操作,可以选择性输出监测结果。</p>
<p>可能大家对于shell脚本比较陌生,在这里推荐官方的shell脚本文档来参考一下</p>
<p><a href="http://wiki.ubuntu.org.cn/Shell%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80" data-original-title="" title="">Ubuntu Shell 编程基础</a></p>
<p>shell脚本的后缀为sh,在任何位置新建一个脚本文件,我选择在 /etc/mysql 目录下新建一个 listen.sh 文件。</p>
<p>执行如下命令:</p>

经过处理后便会变成如下的样子:

1
2
3
4
5
6
7
8
9
10
11
前言
最近发现MySQL服务隔三差五就会挂掉,导致我的网站和爬虫都无法正常运作。自己的网站是基于MySQL,在做爬虫存取一些资料的时候也是基于MySQL,数据量一大了,MySQL它就有点受不了了,时不时会崩掉,虽然我自己有网站监控和邮件通知,但是好多时候还是需要我来手动连接我的服务器重新启动一下我的MySQL,这样简直太不友好了,所以,我就觉定自己写个脚本,定时监控它,如果发现它挂掉了就重启它。
好了,闲言碎语不多讲,开始我们的配置之旅。
运行环境:UbuntuLinux14.04
编写Shell脚本
首先,我们要编写一个shell脚本,脚本主要执行的逻辑如下:
显示mysqld进程状态,如果判断进程未在运行,那么输出日志到文件,然后启动mysql服务,如果进程在运行,那么不执行任何操作,可以选择性输出监测结果。
可能大家对于shell脚本比较陌生,在这里推荐官方的shell脚本文档来参考一下
UbuntuShell编程基础
shell脚本的后缀为sh,在任何位置新建一个脚本文件,我选择在/etc/mysql目录下新建一个listen.sh文件。
执行如下命令:

经过上面的处理,所有乱乱的代码都会被处理好了。

4.保存到数据库

在这里,我们想实现一个通用的方法,就是把存储的一个个内容变成字典的形式,然后执行插入语句的时候,自动构建对应的 sql 语句,插入数据。 比如我们构造如下的字典:

1
2
3
4
5
6
7
8
#构造最佳答案的字典
good_ans_dict = {
"text": good_ans[0],
"answerer": good_ans[1],
"date": good_ans[2],
"is_good": str(good_ans[3]),
"question_id": str(insert_id)
}

构造 sql 语句并插入到数据库的方法如下:

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
#插入数据
def insertData(self, table, my_dict):
try:
self.db.set_character_set('utf8')
cols = ', '.join(my_dict.keys())
values = '"," '.join(my_dict.values())
sql = "INSERT INTO %s (%s) VALUES (%s)" % (table, cols, '"'+values+'"')
try:
result = self.cur.execute(sql)
insert_id = self.db.insert_id()
self.db.commit()
#判断是否执行成功
if result:
return insert_id
else:
return 0
except MySQLdb.Error,e:
#发生错误时回滚
self.db.rollback()
#主键唯一,无法插入
if "key 'PRIMARY'" in e.args[1]:
print self.getCurrentTime(),"数据已存在,未插入数据"
else:
print self.getCurrentTime(),"插入数据失败,原因 %d: %s" % (e.args[0], e.args[1])
except MySQLdb.Error,e:
print self.getCurrentTime(),"数据库错误,原因%d: %s" % (e.args[0], e.args[1])

这里我们只需要传入那个字典,便会构建出对应字典键值和键名的 sql 语句,完成插入。

5.PHP 读取日志

我们将运行结果输出到了日志里,那么怎么查看日志呢?很简单,在这里提供两种方法 方法一: PHP 倒序输出所有日志内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content = "5">
</head>
<body>
<?php
$fp = file("out.log");
if ($fp) {
for($i = count($fp) - 1;$i >= 0; $i --)
echo $fp[$i]."<br>";
}
?>
</body>
</html>

此方法可以看到所有的输入日志,但是如果日志太大了,那么就会报耗费内存太大,无法输出。为此我们就有了第二种方法,利用 linux 命令,输出后十行内容。 方法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content = "5">
</head>
<body>
<?php
$ph = popen('tail -n 100 out.log','r');
while($r = fgets($ph)){
echo $r."<br>";
}
pclose($ph);
?>
</body>
</html>

上面两种方法都是 5 秒刷新一次网页来查看最新的日志。

源代码放送

好了,闲言碎语不多讲,直接上源码了

1
spider.py
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# -*- coding:utf-8 -*-

import urllib
import urllib2
import re
import time
import types
import page
import mysql
import sys
from bs4 import BeautifulSoup

class Spider:

#初始化
def __init__(self):
self.page_num = 1
self.total_num = None
self.page_spider = page.Page()
self.mysql = mysql.Mysql()

#获取当前时间
def getCurrentTime(self):
return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

#获取当前时间
def getCurrentDate(self):
return time.strftime('%Y-%m-%d',time.localtime(time.time()))

#通过网页的页码数来构建网页的URL
def getPageURLByNum(self, page_num):
page_url = "http://iask.sina.com.cn/c/978-all-" + str(page_num) + ".html"
return page_url


#通过传入网页页码来获取网页的HTML
def getPageByNum(self, page_num):
request = urllib2.Request(self.getPageURLByNum(page_num))
try:
response = urllib2.urlopen(request)
except urllib2.URLError, e:
if hasattr(e, "code"):
print self.getCurrentTime(),"获取页面失败,错误代号", e.code
return None
if hasattr(e, "reason"):
print self.getCurrentTime(),"获取页面失败,原因", e.reason
return None
else:
page = response.read().decode("utf-8")
return page

#获取所有的页码数
def getTotalPageNum(self):
print self.getCurrentTime(),"正在获取目录页面个数,请稍候"
page = self.getPageByNum(1)
#匹配所有的页码数,\u4e0b\u4e00\u9875是下一页的UTF8编码
pattern = re.compile(u'<span class="more".*?>.*?<span.*?<a href.*?class="">(.*?)</a>\s*<a.*?\u4e0b\u4e00\u9875</a>', re.S)
match = re.search(pattern, page)
if match:
return match.group(1)
else:
print self.getCurrentTime(),"获取总页码失败"

#分析问题的代码,得到问题的提问者,问题内容,回答个数,提问时间
def getQuestionInfo(self, question):
if not type(question) is types.StringType:
question = str(question)
#print question
pattern = re.compile(u'<span.*?question-face.*?>.*?<img.*?alt="(.*?)".*?</span>.*?<a href="(.*?)".*?>(.*?)</a>.*?answer_num.*?>(\d*).*?</span>.*?answer_time.*?>(.*?)</span>', re.S)
match = re.search(pattern, question)
if match:
#获得提问者
author = match.group(1)
#问题链接
href = match.group(2)
#问题详情
text = match.group(3)
#回答个数
ans_num = match.group(4)
#回答时间
time = match.group(5)
time_pattern = re.compile('\d{4}\-\d{2}\-\d{2}', re.S)
time_match = re.search(time_pattern, time)
if not time_match:
time = self.getCurrentDate()
return [author, href, text, ans_num, time]
else:
return None

#获取全部问题
def getQuestions(self, page_num):
#获得目录页面的HTML
page = self.getPageByNum(page_num)
soup = BeautifulSoup(page)
#分析获得所有问题
questions = soup.select("div.question_list ul li")
#遍历每一个问题
for question in questions:
#获得问题的详情
info = self.getQuestionInfo(question)
if info:
#得到问题的URL
url = "http://iask.sina.com.cn/" + info[1]
#通过URL来获取问题的最佳答案和其他答案
ans = self.page_spider.getAnswer(url)
print self.getCurrentTime(),"当前爬取第",page_num,"的内容,发现一个问题",info[2],"回答数量",info[3]
#构造问题的字典,插入问题
ques_dict = {
"text": info[2],
"questioner": info[0],
"date": info[4],
"ans_num": info[3],
"url": url
}
#获得插入的问题的自增ID
insert_id = self.mysql.insertData("iask_questions",ques_dict)
#得到最佳答案
good_ans = ans[0]
print self.getCurrentTime(),"保存到数据库,此问题的ID为",insert_id
#如果存在最佳答案,那么就插入
if good_ans:
print self.getCurrentTime(),insert_id,"号问题存在最佳答案",good_ans[0]
#构造最佳答案的字典
good_ans_dict = {
"text": good_ans[0],
"answerer": good_ans[1],
"date": good_ans[2],
"is_good": str(good_ans[3]),
"question_id": str(insert_id)
}
#插入最佳答案
if self.mysql.insertData("iask_answers",good_ans_dict):
print self.getCurrentTime(),"保存最佳答案成功"
else:
print self.getCurrentTime(),"保存最佳答案失败"
#获得其他答案
other_anses = ans[1]
#遍历每一个其他答案
for other_ans in other_anses:
#如果答案存在
if other_ans:
print self.getCurrentTime(),insert_id,"号问题存在其他答案",other_ans[0]
#构造其他答案的字典
other_ans_dict = {
"text": other_ans[0],
"answerer": other_ans[1],
"date": other_ans[2],
"is_good": str(other_ans[3]),
"question_id": str(insert_id)
}
#插入这个答案
if self.mysql.insertData("iask_answers",other_ans_dict):
print self.getCurrentTime(),"保存其他答案成功"
else:
print self.getCurrentTime(),"保存其他答案失败"

#主函数
def main(self):
f_handler=open('out.log', 'w')
sys.stdout=f_handler
page = open('page.txt', 'r')
content = page.readline()
start_page = int(content.strip()) - 1
page.close()
print self.getCurrentTime(),"开始页码",start_page
print self.getCurrentTime(),"爬虫正在启动,开始爬取爱问知识人问题"
self.total_num = self.getTotalPageNum()
print self.getCurrentTime(),"获取到目录页面个数",self.total_num,"个"
if not start_page:
start_page = self.total_num
for x in range(1,start_page):
print self.getCurrentTime(),"正在抓取第",start_page-x+1,"个页面"
try:
self.getQuestions(start_page-x+1)
except urllib2.URLError, e:
if hasattr(e, "reason"):
print self.getCurrentTime(),"某总页面内抓取或提取失败,错误原因", e.reason
except Exception,e:
print self.getCurrentTime(),"某总页面内抓取或提取失败,错误原因:",e
if start_page-x+1 < start_page:
f=open('page.txt','w')
f.write(str(start_page-x+1))
print self.getCurrentTime(),"写入新页码",start_page-x+1
f.close()

spider = Spider()
spider.main()
1
page.py
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# -*- coding:utf-8 -*-
import urllib
import urllib2
import re
import time
import types
import tool
from bs4 import BeautifulSoup

#抓取分析某一问题和答案
class Page:

def __init__(self):
self.tool = tool.Tool()

#获取当前时间
def getCurrentDate(self):
return time.strftime('%Y-%m-%d',time.localtime(time.time()))

#获取当前时间
def getCurrentTime(self):
return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

#通过页面的URL来获取页面的代码
def getPageByURL(self, url):
try:
request = urllib2.Request(url)
response = urllib2.urlopen(request)
return response.read().decode("utf-8")
except urllib2.URLError, e:
if hasattr(e, "code"):
print self.getCurrentTime(),"获取问题页面失败,错误代号", e.code
return None
if hasattr(e, "reason"):
print self.getCurrentTime(),"获取问题页面失败,原因", e.reason
return None


#传入一个List,返回它的标签里的内容,如果为空返回None
def getText(self, html):
if not type(html) is types.StringType:
html = str(html)
#提取出<pre>标签里的内容
pattern = re.compile('<pre.*?>(.*?)</pre>', re.S)
match = re.search(pattern, html)
#如果匹配成功
if match:
return match.group(1)
else:
return None

#传入最佳答案的HTML,分析出回答者和回答时间
def getGoodAnswerInfo(self, html):
pattern = re.compile('"answer_tip.*?<a.*?>(.*?)</a>.*?<span class="time.*?>.*?\|(.*?)</span>', re.S)
match = re.search(pattern, html)
#如果匹配,返回回答者和回答时间
if match:
time = match.group(2)
time_pattern = re.compile('\d{2}\-\d{2}\-\d{2}', re.S)
time_match = re.search(time_pattern, time)
if not time_match:
time = self.getCurrentDate()
else:
time = "20"+time
return [match.group(1),time]
else:
return [None,None]

#获得最佳答案
def getGoodAnswer(self, page):
soup = BeautifulSoup(page)
text = soup.select("div.good_point div.answer_text pre")
if len(text) > 0:
#获得最佳答案的内容
ansText = self.getText(str(text[0]))
ansText = self.tool.replace(ansText)
#获得最佳答案的回答者信息
info = soup.select("div.good_point div.answer_tip")
ansInfo = self.getGoodAnswerInfo(str(info[0]))
#将三者组合成一个List
answer = [ansText, ansInfo[0], ansInfo[1],1]
return answer
else:
#如果不存在最佳答案,那么就返回空
return None

#传入回答者HTML,分析出回答者,回答时间
def getOtherAnswerInfo(self, html):
if not type(html) is types.StringType:
html = str(html)
pattern = re.compile('"author_name.*?>(.*?)</a>.*?answer_t">(.*?)</span>', re.S)
match = re.search(pattern, html)
#获得每一个回答的回答者信息和回答时间
if match:
time = match.group(2)
time_pattern = re.compile('\d{2}\-\d{2}\-\d{2}', re.S)
time_match = re.search(time_pattern, time)
if not time_match:
time = self.getCurrentDate()
else:
time = "20"+time
return [match.group(1),time]
else:
return [None,None]


#获得其他答案
def getOtherAnswers(self, page):
soup = BeautifulSoup(page)
results = soup.select("div.question_box li.clearfix .answer_info")
#所有答案,包含好多个List,每个List包含了回答内容,回答者,回答时间
answers = []
for result in results:
#获得回答内容
ansSoup = BeautifulSoup(str(result))
text = ansSoup.select(".answer_txt span pre")
ansText = self.getText(str(text[0]))
ansText = self.tool.replace(ansText)
#获得回答者和回答时间
info = ansSoup.select(".answer_tj")
ansInfo = self.getOtherAnswerInfo(info[0])
#将三者组合成一个List
answer = [ansText, ansInfo[0], ansInfo[1],0]
#加入到answers
answers.append(answer)
return answers

#主函数
def getAnswer(self, url):
if not url:
url = "http://iask.sina.com.cn/b/gQiuSNCMV.html"
page = self.getPageByURL(url)
good_ans = self.getGoodAnswer(page)
other_ans = self.getOtherAnswers(page)
return [good_ans,other_ans]


page = Page()
page.getAnswer(None)
1
tool.py
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
#-*- coding:utf-8 -*-
import re

#处理页面标签类
class Tool:

#将超链接广告剔除
removeADLink = re.compile('<div class="link_layer.*?</div>')
#去除img标签,1-7位空格,&nbsp;
removeImg = re.compile('<img.*?>| {1,7}|&nbsp;')
#删除超链接标签
removeAddr = re.compile('<a.*?>|</a>')
#把换行的标签换为\n
replaceLine = re.compile('<tr>|<div>|</div>|</p>')
#将表格制表<td>替换为\t
replaceTD= re.compile('<td>')
#将换行符或双换行符替换为\n
replaceBR = re.compile('<br><br>|<br>')
#将其余标签剔除
removeExtraTag = re.compile('<.*?>')
#将多行空行删除
removeNoneLine = re.compile('\n+')

def replace(self,x):
x = re.sub(self.removeADLink,"",x)
x = re.sub(self.removeImg,"",x)
x = re.sub(self.removeAddr,"",x)
x = re.sub(self.replaceLine,"\n",x)
x = re.sub(self.replaceTD,"\t",x)
x = re.sub(self.replaceBR,"\n",x)
x = re.sub(self.removeExtraTag,"",x)
x = re.sub(self.removeNoneLine,"\n",x)
#strip()将前后多余内容删除
return x.strip()
1
mysql.py
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
# -*- coding:utf-8 -*-


import MySQLdb
import time

class Mysql:

#获取当前时间
def getCurrentTime(self):
return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

#数据库初始化
def __init__(self):
try:
self.db = MySQLdb.connect('ip','username','password','db_name')
self.cur = self.db.cursor()
except MySQLdb.Error,e:
print self.getCurrentTime(),"连接数据库错误,原因%d: %s" % (e.args[0], e.args[1])

#插入数据
def insertData(self, table, my_dict):
try:
self.db.set_character_set('utf8')
cols = ', '.join(my_dict.keys())
values = '"," '.join(my_dict.values())
sql = "INSERT INTO %s (%s) VALUES (%s)" % (table, cols, '"'+values+'"')
try:
result = self.cur.execute(sql)
insert_id = self.db.insert_id()
self.db.commit()
#判断是否执行成功
if result:
return insert_id
else:
return 0
except MySQLdb.Error,e:
#发生错误时回滚
self.db.rollback()
#主键唯一,无法插入
if "key 'PRIMARY'" in e.args[1]:
print self.getCurrentTime(),"数据已存在,未插入数据"
else:
print self.getCurrentTime(),"插入数据失败,原因 %d: %s" % (e.args[0], e.args[1])
except MySQLdb.Error,e:
print self.getCurrentTime(),"数据库错误,原因%d: %s" % (e.args[0], e.args[1])

数据库建表 SQL 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE IF NOT EXISTS `iask_answers` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`text` text NOT NULL COMMENT '回答内容',
`question_id` int(18) NOT NULL COMMENT '问题ID',
`answerer` varchar(255) NOT NULL COMMENT '回答者',
`date` varchar(255) NOT NULL COMMENT '回答时间',
`is_good` int(11) NOT NULL COMMENT '是否是最佳答案',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `iask_questions` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '问题ID',
`text` text NOT NULL COMMENT '问题内容',
`questioner` varchar(255) NOT NULL COMMENT '提问者',
`date` date NOT NULL COMMENT '提问时间',
`ans_num` int(11) NOT NULL COMMENT '回答数量',
`url` varchar(255) NOT NULL COMMENT '问题链接',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

运行的时候执行如下命令即可

1
nohup python spider.py &

代码写的不好,仅供大家学习参考使用,如有问题,欢迎留言交流。

运行结果查看

我们把 PHP 文件和 log 文件放在同一目录下,运行 PHP 文件,便可以看到如下的内容: 20150908222744 小伙伴们赶快试一下吧。

Other

前言

最近发现 MySQL 服务隔三差五就会挂掉,导致我的网站和爬虫都无法正常运作。自己的网站是基于 MySQL,在做爬虫存取一些资料的时候也是基于 MySQL,数据量一大了,MySQL 它就有点受不了了,时不时会崩掉,虽然我自己有网站监控和邮件通知,但是好多时候还是需要我来手动连接我的服务器重新启动一下我的 MySQL,这样简直太不友好了,所以,我就觉定自己写个脚本,定时监控它,如果发现它挂掉了就重启它。 好了,闲言碎语不多讲,开始我们的配置之旅。 运行环境:Ubuntu Linux 14.04

编写 Shell 脚本

首先,我们要编写一个 shell 脚本,脚本主要执行的逻辑如下: 显示 mysqld 进程状态,如果判断进程未在运行,那么输出日志到文件,然后启动 mysql 服务,如果进程在运行,那么不执行任何操作,可以选择性输出监测结果。 可能大家对于 shell 脚本比较陌生,在这里推荐官方的 shell 脚本文档来参考一下 Ubuntu Shell 编程基础 shell 脚本的后缀为 sh,在任何位置新建一个脚本文件,我选择在 /etc/mysql 目录下新建一个 listen.sh 文件。 执行如下命令:

1
2
3
cd /etc/mysql
touch listen.sh
vi listen.sh

进入到 vi 中,我们添加如下脚本内容:

1
2
3
4
5
6
7
8
9
#!/bin/bash
pgrep mysqld &> /dev/null
if [ $? -gt 0 ]
then
echo "`date` mysql is stop"
service mysql start
else
echo "`date` mysql running"
fi

其中 pgrep mysqld 是监测 mysqld 服务的运行状态,&> /dev/null 是将其结果输出到空文件,也就是不保存输出信息 $? 是拿到上一条命令的运行结果,-gt 0 是判断是否大于 0,后面则是输出时间到日志文件,然后启动 mysql,否则不启动 mysql 保存好了,那么我们执行如下的命令,来测试一下。 20150816164552 贴心的命令文字版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@iZ28uogb3laZ:/etc/mysql# vi listen.sh
root@iZ28uogb3laZ:/etc/mysql# pgrep mysqld
3359
root@iZ28uogb3laZ:/etc/mysql# chmod 777 listen.sh
root@iZ28uogb3laZ:/etc/mysql# ./listen.sh
Sun Aug 16 16:44:58 CST 2015 mysql running
root@iZ28uogb3laZ:/etc/mysql# sudo service mysql stop
mysql stop/waiting
root@iZ28uogb3laZ:/etc/mysql# ./listen.sh
Sun Aug 16 16:45:17 CST 2015 mysql is stop
mysql start/running, process 4084
root@iZ28uogb3laZ:/etc/mysql# ./listen.sh
Sun Aug 16 16:45:24 CST 2015 mysql running
root@iZ28uogb3laZ:/etc/mysql#

嗯,编辑完了.sh 文件之后,我们首先要对其进行授权,增加可执行的权限。

1
sudo chmod 777 listen.sh

然后运行脚本测试一下,显示 mysql 正在运行。把 mysql 关掉,运行脚本,便会检测到 mysql 已关闭,然后重新启动了 mysql,再次运行,便会发现 mysql 正常运行了。 注:这里我比较纳闷 shell 脚本中 pgrep mysqld 的返回结果是什么。为什么它大于 0 便代表 mysql 服务挂掉了呢?感觉逻辑有点是相反的,不过实测可用有效。如果大家明白为什么,非常希望您可以给我一个解释。

修改日志输出

好,接下来我们把输出的内容保存到日志里。修改脚本文件如下

1
2
3
4
5
6
7
8
9
#!/bin/bash
pgrep mysqld &> /dev/null
if [ $? -gt 0 ]
then
echo "`date` mysql is stop" >> /var/log/mysql_listen.log
service mysql start
else
echo "`date` mysql running" >> /var/log/mysql_listen.log
fi

这样,每执行一次脚本,输出结果都会被保存到 /var/log/mysql_listen.log 中了。

添加定时任务

好了,脚本可以顺利执行了,那么我们就需要定时调用一下这个脚本来运行了,我们需要用到 cron。 首先我们需要编辑一下 corn 调度表格,命令如下:

1
crontab -e

如果你是第一次编辑这个,他会让你选择文件打开方式,随便选一个数字就好了。 比如我们用 GNU 打开的,我们就在它的最后一行添加下面的一句话即可。 20150816165506、 文字版本:

1
*/5 * * * * /etc/mysql/mysql_listen.sh

/5 代表五分钟执行一次,后面的四个点依次代表了,小时,日,月,星期。如果想要时间长一些,比如一小时调度一次,那就设置一下后面第一个*就好了。 好,保存一下,重启 cron 服务。

1
service cron restart

嗯,调度任务已经添加进去了,这样,每五分钟系统就会调用一下刚才写的那个脚本。 过一段时间,我们来看一下运行效果,嗯,监控跑的很顺利呐。 20150816165858 文字版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Sun Aug 16 15:39:12 CST 2015 mysql running
Sun Aug 16 15:40:01 CST 2015 mysql running
Sun Aug 16 15:45:02 CST 2015 mysql running
Sun Aug 16 15:50:01 CST 2015 mysql running
Sun Aug 16 15:55:01 CST 2015 mysql running
Sun Aug 16 16:00:01 CST 2015 mysql running
Sun Aug 16 16:05:01 CST 2015 mysql running
Sun Aug 16 16:10:01 CST 2015 mysql running
Sun Aug 16 16:15:01 CST 2015 mysql running
Sun Aug 16 16:20:01 CST 2015 mysql running
Sun Aug 16 16:25:01 CST 2015 mysql running
Sun Aug 16 16:30:01 CST 2015 mysql running
Sun Aug 16 16:35:01 CST 2015 mysql running
Sun Aug 16 16:40:01 CST 2015 mysql running
Sun Aug 16 16:51:04 CST 2015 mysql running

哈哈,是不是五分钟监测了一次呢?大功告成。

后记

这样,我们就实现了五分钟定时检测 MySQL 进程服务,妈妈再也不用担心我的网站会宕掉啦。 如有问题,欢迎留言交流,谢谢啦。

C/C++

最近在准备C语言的上级考试,之前对C接触不多,在练习过程中把一些小知识点记录下来。

1.字符串的截取

利用strncpy函数,传入三个参数,分别为目标字符串,起始位置,长度。 例如将日期字符串转化为数字,如20120112

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main() {

char date1[20],date2[20];
scanf("%s",&date1);
scanf("%s",&date2);
char temp[4];
int year1 = atoi(strncpy(temp,date1,4));
int year2 = atoi(strncpy(temp,date2,4));
printf("year1:%d\n",year1);
printf("year2:%d\n",year2);
char temp2[2];
int month1 = atoi(strncpy(temp2,date1+4,2));
int month2 = atoi(strncpy(temp2,date2+4,2));
printf("month1:%d\n",month1);
printf("month2:%d\n",month2);
int day1 = atoi(strncpy(temp2,date1+6,2));
int day2 = atoi(strncpy(temp2,date2+6,2));
printf("day1:%d\n",day1);
printf("day2:%d\n",day2);
return 0;

}

以上便实现了输入一个日期然后对其进行分割的操作。

2.二维数组的动态声明

利用malloc可以实现数组的动态声明

1
2
3
4
5
6
7
8
9
int **a;
a = (int **)malloc(2*sizeof(int *));
int i,j;
for (i = 0; i < 2; i ++) {
a[i] = (int *)malloc(3*sizeof(int));
for (j = 0; j < 3; j++) {
scanf("%d",&a[i][j]);
}
}

以上便实现了动态数组的分配,利用scanf为数组赋值

3.二维数组的声明和初始化

头文件

1
#include <memory.h>

初始化和测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int result[2][2];
for (i = 0; i < 2; i ++) {
for (j = 0; j < 2; j++) {
printf("%d ",result[i][j]);
}
printf("\n");
}
memset(result,0,sizeof(int)*4);
for (i = 0; i < 2; i ++) {
for (j = 0; j < 2; j++) {
printf("%d ",result[i][j]);
}
printf("\n");
}

结果

1
2
3
4
4196944 0 
4195696 0
0 0
0 0

上述是数组的非动态声明

4.快速排序

假设要排序的数组是A[1]……A[N],首先任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。一趟快速排序的算法是: 1)设置两个变量I、J,排序开始的时候 I=0,J=N-1; 2)以第一个数组元素作为关键数据,赋值给X,即X=A[0]; 3)从J开始向前搜索,即由后开始向前搜索,找到第一个小于X的值,两者交换; 4)从I开始向后搜索,即由前开始向后搜索,找到第一个大于X的值,两者交换; 5)重复第3、4步,直到I=J; 例如:待排序的数组A的值分别是:(初始关键数据X:=49) A[0] A[1] A[2] A[3] A[4] A[5] A[6] 49 38 65 97 76 13 27 进行第一次交换后: 27 38 65 97 76 13 49 ( 按照算法的第三步从后面开始找 ) 进行第二次交换后: 27 38 49 97 76 13 65 ( 按照算法的第四步从前面开始找>X的值,65>49,两者交换,此时I=3 ) 进行第三次交换后: 27 38 13 97 76 49 65 ( 按照算法的第五步将又一次执行算法的第三步从后开始找) 进行第四次交换后: 27 38 13 49 76 97 65 ( 按照算法的第四步从前面开始找大于X的值,97>49,两者交换,此时J=4 ) 此时再执行第三不的时候就发现I=J,从而结束一躺快速排序,那么经过一躺快速排序之后: 27 38 13 49 76 97 65 即所有大于49的数全部在49的后面,所有小于49的数全部在49的前面。

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
#include <stdio.h>

void quiksort(int a[],int low,int high)
{
int i = low;
int j = high;
int temp = a[i];

if( low < high)
{
while(i < j)
{
while((a[j] >= temp) && (i < j))
{
j--;
}
a[i] = a[j];
while((a[i] <= temp) && (i < j))
{
i++;
}
a[j]= a[i];
}
a[i] = temp;
quiksort(a,low,i-1);
quiksort(a,j+1,high);
}
else
{
return;
}
}

int main()
{
int arr[5] = {23,1,21,4,19};
quiksort(arr,0,4);
int i;
for(i=0;i<5;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}

快速排序代码如上

5.字符串拷贝

小例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello";
char *a = "world";
strcpy(str,a);
printf("%s",str);
printf("\n");
system("pause");
return 0;
}

运行结果:world,即把后者完全覆盖前者。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char *str = new char[6];
char *a = "world";
strcpy(str,a);
printf("%s", str);
printf("\n");
system("pause");
return 0;
}

运行结果一致 某一长度的字符串截取

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char *str = new char[6];
char *a = "world";
strncpy(str,a+1,5);
printf("%s", str);
printf("\n");
system("pause");
return 0;
}

运行结果:orld

6.字符串的拼接

小例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello ";
char *a = "world";
char *x = strcat(str,a);
printf("%s\n", x);
printf("%s\n", x);
printf("\n");
system("pause");
return 0;
}

运行结果: hello world hello world 此函数既返回结果,又将目标字符串赋值

7.字符串查找匹配

例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello ";
char *x = strchr(str,'e');
printf("%d\n", x - str);
printf("%s\n",x);
printf("\n");
system("pause");
return 0;
}

运行结果: 1 ello

8.字符串比较

例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello ";
char str2[20] = "hello2";
int x = strcmp(str, str2);
printf("%d\n",x);
printf("\n");
system("pause");
return 0;
}

运行结果-1 忽略大小写

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello";
char str2[20] = "Hello";
int x = stricmp(str, str2);
printf("%d\n",x);
printf("\n");
system("pause");
return 0;
}

运行结果0

9.字符串分割

示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
char test1[] = "feng,ke,wei";
char x[3][30];
char *p;
p = strtok(test1, ",");
int count = 0;
while (p) {
printf("%s\n", p);
strcpy(x[count],p);
count++;
p = strtok(NULL, ",");
}
for (int i = 0; i<count; i ++) {
printf("%s ", x[i]);
}
system("pause");
return 0;
}

运行结果 feng ke wei feng ke wei

10.格式化输出几位小数

例如

1
printf("%.5f",18.223);

则是输出5位小数 又如

1
printf("%5.1f",1.2345);

则是控制总位数为5,小数点后为1位,不够的在前面补空格

C/C++

本节任务

本次我们需要完成的任务是 完成两台主机通过中间主机的数据通信(网络层)

  • 增加基于IP地址的转发功能
  • 增加网络层封装

其实最主要的就是基于IP地址的转发功能,网络层的封装其实我们在初级功能中就已经做好了。

原理

首先,实验的思路是A通过中间主机B向C发送数据。那么B则作为一个路由器,B要监听两个网卡,一个网卡发来的数据通过另一个网卡发出去。 示意图如下: A————->B1===B2——————>C 从图上可以看出,B主机的两个网卡数据互通,A和B1则处于一个局域网内,B2和C处于另一个局域网内。 就比如这样,现在室友A在用有线上网,我的电脑B也在用有线上网,我们的有线处在同一局域网,我的电脑B同时散着一个无线网,我的手机C又连接到了这个无线上。 那么要实现A到C的数据传送,即模拟室友A要发送数据到我的手机C,那么流程则是这样的: 室友A在有线局域网发送数据到我的网卡B1,B1将数据通过网卡B2转发到无线局域网,通过无线局域网到达我的手机C。 A的发送要构建一个帧,目的MAC地址为B1,目的IP为C。B则要开启两个网卡,B1监听接收数据,B2网卡则要用ARP协议扫描所在无线局域网内的IP和MAC,B获取到了A发来的帧之后,解析它的IP地址和MAC地址,匹配刚才扫描得到的IP和MAC对应表,将源MAC换成B2网卡MAC,目的MAC换成C的MAC,IP不变,数据data不变。构建新帧之后发送出去。 好啦,思路大体就是这样。

实战

需要三个程序,一个是发送,一个路由,一个接收。所以一共三个程序要同时运行起来执行。 以上是我的大体思路,如有错误,还请指正。现已用代码实现完毕。 代码暂不公开,只提供部分重点代码解析:

一、发送端

其实发送端和初级功能的发送差不多 个人编写的交互流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
IP地址:121.250.216.221   MAC地址:3c970e4b56d6con:127

-------------------------------------------
IP地址:121.250.216.227 MAC地址:089e01b948f4con:128

-------------------------------------------
IP地址:121.250.216.228 MAC地址:10bf48705aeecon:129

获取MAC地址完毕,请输入你要发送对方的IP地址:
192.168.1.3
请输入你要发送的内容:
im cqc
要发送的内容:im cqc

具体代码不再解析,同上一篇初级功能。

二、路由端

首先要开启两个网卡,声明两个网卡对象和处理器

1
2
pcap_if_t  *d,*d2;					//选中的网络适配器
pcap_t *adhandle,*adhandle2; //捕捉实例,是pcap_open返回的对象,adhandle是用来发送数据,adhandle2是用来接收数据

一个用来接收一个用来发送,这里定义了adhandle是用来发送,adhandle2是用来接收数据。 那么打开适配器就在main方法中,提前打开两个网卡

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
int num;
printf("请输入你要转发数据的网卡代号:\n");
//让用户选择选择哪个适配器进行转发
scanf_s("%d",&num);

//跳转到选中的适配器
for(d=alldevs, i=0; i< num-1 ; d=d->next, i++);

//运行到此处说明用户的输入是合法的,找到发送数据网卡
if((adhandle = pcap_open(d->name, //设备名称
65535, //存放数据包的内容长度
PCAP_OPENFLAG_PROMISCUOUS, //混杂模式
1000, //超时时间
NULL, //远程验证
errbuf //错误缓冲
)) == NULL){
//打开适配器失败,打印错误并释放适配器列表
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
// 释放设备列表
pcap_freealldevs(alldevs);
return -1;
}

int num2;
printf("请输入你要接收数据的网卡代号:");
//让用户选择用哪个网卡来收数据
scanf_s("%d",&num2);
//用户输入的数字超出合理范围


//跳转到选中的适配器
for(d2=alldevs, i=0; i< num2-1 ; d2=d2->next, i++);

//运行到此处说明用户的输入是合法的
if((adhandle2 = pcap_open(d2->name, //设备名称
65535, //存放数据包的内容长度
PCAP_OPENFLAG_PROMISCUOUS, //混杂模式
1000, //超时时间
NULL, //远程验证
errbuf //错误缓冲
)) == NULL){
//打开适配器失败,打印错误并释放适配器列表
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d2->name);

接下来用用于发送的handle处理器来扫描它的局域网IP,获得局域网内的MAC地址,记录在一个表中,存放IP和MAC的对应关系。 这个表可以用结构体数组来保存,比如可以这样:

1
2
3
4
struct ip_mac_list{
IpAddress ip;
unsigned char mac[6];
};
1
ip_mac_list  list[256];                       //存储IP和MAC地址的对应表

那么以上便是准备工作,我们完成了两个网卡的打开,发送网卡扫描获取局域网MAC,接下来便是最重要的监听加转发。 那么这个怎办?那就开一个新线程。 让我们声明一个新的路由线程。

1
DWORD WINAPI RouteThread(LPVOID lpParameter);

那么线程要接收进来什么参数呢? 首先必须要的是两个网卡处理器,在main方法中已经做好初始化的adhandle和adhandle2,另外还有alldevs,可以持有这个指针来释放设备列表,出现错误时释放资源并退出。 初级功能中声明过了 struct sparam sp; struct gparam gp; 这两个就是发送ARP线程和接收ARP线程中的两个参数,那么仿照这个功能,我们定义一个新的结构体

1
2
3
4
5
struct rparam{
pcap_t *adhandle_rec;
pcap_t *adhandle_send;
pcap_if_t * alldevs; //所有网络适配器
};

在main方法中把它来初始化赋值

1
2
3
rp.adhandle_send = adhandle;
rp.adhandle_rec = adhandle2;
rp.alldevs = alldevs;

当做参数传入这个线程

1
2
routethread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) RouteThread, &rp,
0, NULL);

其中第四个参数就是传递了这个结构体进去。注意这个语句最好不要直接放在main方法中直接调用,可以在全部获取完MAC地址之后再开启这个线程。 那么接下来就说一下这个线程都干了些什么,只简略说一下核心部分。 首先开启了这个线程之后会一直都在执行,那么就可以加入

1
while((res = pcap_next_ex(adhandle2,&header,&pkt_data))>=0)

这样的while判断语句来一直监听数据包的接收,然后解析数据。

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
ethernet =  (EthernetHeader *)(pkt_data);
for(int i=0;i<6;i++){
sou_mac[i] = ethernet->SourMAC[i];
}
for(int i=0;i<6;i++){
des_mac[i] = ethernet->DestMAC[i];
}
// 获得IP数据包头部的位置
ip = (IpHeader *) (pkt_data +14); //14为以太网帧头部长度
//获得TCP头部的位置
ip_len = (ip->Version_HLen & 0xf) *4;
tcp = (TcpHeader *)((u_char *)ip+ip_len);
data = (char *)((u_char *)tcp+20);
printf("data:%s\n",data);
printf("ip:");
printf("%d.%d.%d.%d -> %d.%d.%d.%d\n",
ip->SourceAddr.byte1,
ip->SourceAddr.byte2,
ip->SourceAddr.byte3,
ip->SourceAddr.byte4,
ip->DestinationAddr.byte1,
ip->DestinationAddr.byte2,
ip->DestinationAddr.byte3,
ip->DestinationAddr.byte4);
printf("sou_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", sou_mac[0], sou_mac[1], sou_mac[2],
sou_mac[3], sou_mac[4], sou_mac[5]);
printf("des_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", des_mac[0], des_mac[1], des_mac[2],
des_mac[3], des_mac[4], des_mac[5]);

然后接下来每接收到一个数据,就进行构建新的帧转发出去,目的MAC先匹配list表,如果list没有找到,那么我让他指定了一个mac,比如广播MAC。源MAC地址则赋值网卡的MAC地址。 注意,传统以太网中数据长度为45-1500,那么我在构建前把解析出的data作了下判断长度再构建,因为我已经把sendbuffer声明为一个固定长度了,为了防止越界,我先进行一个长度判断。

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
//以下开始构建帧发送
//首先判断data最大值小于1500
if(strlen(data)<1500){
//目的MAC
BYTE send_destmac[6];
bool findMac = false;
for(int c = 0;c<con;c++){
if(ip->DestinationAddr.byte1 == list[c].ip.byte1&&
ip->DestinationAddr.byte2 == list[c].ip.byte2&&
ip->DestinationAddr.byte3 == list[c].ip.byte3&&
ip->DestinationAddr.byte4 == list[c].ip.byte4)
{
printf("Find its MAC!\n");
findMac = true;
send_destmac[0] = list[c].mac[0];
send_destmac[1] = list[c].mac[1];
send_destmac[2] = list[c].mac[2];
send_destmac[3] = list[c].mac[3];
send_destmac[4] = list[c].mac[4];
send_destmac[5] = list[c].mac[5];
}
}
if(!findMac){
send_destmac[0] = 0xff;
send_destmac[1] = 0xff;
send_destmac[2] = 0xff;
send_destmac[3] = 0xff;
send_destmac[4] = 0xff;
send_destmac[5] = 0xff;
}
printf("destmac:%02x-%02x-%02x-%02x-%02x-%02x\n",
send_destmac[0],send_destmac[1],send_destmac[2],
send_destmac[3],send_destmac[4],send_destmac[5]
);
memcpy(send_ethernet.DestMAC, send_destmac, 6);
//源MAC地址
BYTE send_hostmac[6];
//源MAC地址
send_hostmac[0] = local_mac[0]; //赋值本地MAC地址
send_hostmac[1] = local_mac[1];
send_hostmac[2] = local_mac[2];
send_hostmac[3] = local_mac[3];
send_hostmac[4] = local_mac[4];
send_hostmac[5] = local_mac[5];
//赋值源MAC地址
memcpy(send_ethernet.SourMAC, send_hostmac, 6);
send_ethernet.EthType = htons(0x0800);
//赋值SendBuffer
memcpy(&SendBuffer, &send_ethernet, sizeof(struct EthernetHeader));

以上只是赋值了帧头,至于IP头,TCP头,数据的赋值就参照初级功能的来赋值吧,不要忘了校验和的检验。好,大体上就是这样,接受来数据包并转发出去的原理就是这样。

三、接收

不用多改,就是初级功能中的接收,在此写一写小小的优化措施,防止接收到过多的数据帧而造成不断乱蹦,导致你看不到接收的东西。 在打印的时候加一个过滤就好了。部分代码如下: 在main方法中提示用户输入要接收的IP地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
printf("请输入要接收的IP地址,输入0.0.0.0代表全部接收,请输入\n");
bool receiveAll = false;
u_int ip1,ip2,ip3,ip4;
bool legal = false;
while(!legal){
scanf_s("%d.%d.%d.%d",&ip1,&ip2,&ip3,&ip4);
if(ip1==0&&ip2==0&&ip3==0&&ip4==0){
receiveAll = true;
legal = true;
break;
}
if(ip1<0||ip1>255||ip2<0||ip2>255||ip3<0||ip3>255||ip4<1||ip4>254){
legal = false;
printf("对不起,IP输入不合法,请重新输入:\n");
}else{
legal = true;
}
}

打印时的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if(receiveAll||(ip->SourceAddr.byte1==ip1&&
ip->SourceAddr.byte2==ip2&&
ip->SourceAddr.byte3==ip3&&
ip->SourceAddr.byte4==ip4)){
printf("%d.%d.%d.%d.%d -> %d.%d.%d.%d.%d\n",
ip->SourceAddr.byte1,
ip->SourceAddr.byte2,
ip->SourceAddr.byte3,
ip->SourceAddr.byte4,
sport,
ip->DestinationAddr.byte1,
ip->DestinationAddr.byte2,
ip->DestinationAddr.byte3,
ip->DestinationAddr.byte4,
dport);
printf("sou_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", sou_mac[0], sou_mac[1], sou_mac[2],
sou_mac[3], sou_mac[4], sou_mac[5]);
printf("des_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", des_mac[0], des_mac[1], des_mac[2],
des_mac[3], des_mac[4], des_mac[5]);
printf("%s\n",data);
printf("-----------------------------------------------------\n");
}

好,代码就先放送这么多,具体的实现只要有了思路我相信肯定不难,如有问题,欢迎与我交流。

C/C++

学习文档

这里我们直接进入正题吧,关于Winpcap的基础知识讲解这里就不再赘述了。在这里给大家提供一些学习网址 Winpcap网络编程及通信教程Winpcap中文技术文档

学习内容

  • 获取设备列表
  • 获取已安装设备的高级信息
  • 打开适配器并捕获数据包
  • 不用回调方法捕获数据包
  • 过滤数据包
  • 分析数据包
  • 处理脱机堆文件
  • 发送数据包
  • 收集并统计网络流量

这些内容,在上述两个链接中均已经有了比较详细的讲解,希望对大家有帮助。

两台主机通信实战

完成两台主机之间的数据通信(数据链路层)

  • 仿真ARP协议获得网段内主机的MAC表
  • 使用帧完成两台主机的通信(Hello! I’m …)

首先我们要理解ARP是干嘛的,ARP主要作用就是通过IP地址来获取MAC地址。那么怎样获取呢?本机向局域网内主机发送ARP包,ARP包内包含了目的IP,源IP,目的MAC,源MAC,其中目的MAC地址为广播地址,FF-FF-FF-FF-FF-FF,即向局域网内所有主机发送一个ARP请求,那么其他主机收到这个请求之后则会向请求来源返回一个数据包。在这个返回的数据包中包含了自身的MAC地址。那么本机收到这些返回的数据包进行解析之后便会得到局域网内所有主机的MAC地址了.. 编程开始: 新建一个C++项目,配好环境,引入Winpcap相关的库,这些不再赘述。 头文件引入

1
2
3
4
5
#define HAVE_REMOTE
#define WPCAP
#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>

在main函数中首先声明一系列变量如下

1
2
3
char *ip_addr;                                    //IP地址
char *ip_netmask; //子网掩码
unsigned char *ip_mac; //本机MAC地址

为这三个变量分配地址空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ip_addr = (char *) malloc(sizeof(char) * 16); //申请内存存放IP地址
if (ip_addr == NULL)
{
printf("申请内存存放IP地址失败!\n");
return -1;
}
ip_netmask = (char *) malloc(sizeof(char) * 16); //申请内存存放NETMASK地址
if (ip_netmask == NULL)
{
printf("申请内存存放NETMASK地址失败!\n");
return -1;
}
ip_mac = (unsigned char *) malloc(sizeof(unsigned char) * 6); //申请内存存放MAC地址
if (ip_mac == NULL)
{
printf("申请内存存放MAC地址失败!\n");
return -1;
}

接下来就是烂大街的程序,获取适配器列表并选中相应的适配器,注释已经在代码中了,如果还有不明白的请参照前几次的讲解。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//获取本地适配器列表
if(pcap_findalldevs_ex(PCAP_SRC_IF_STRING,NULL,&alldevs,errbuf) == -1){
//结果为-1代表出现获取适配器列表失败
fprintf(stderr,"Error in pcap_findalldevs_ex:\n",errbuf);
//exit(0)代表正常退出,exit(other)为非正常退出,这个值会传给操作系统
exit(1);
}


for(d = alldevs;d !=NULL;d = d->next){
printf("-----------------------------------------------------------------\nnumber:%d\nname:%s\n",++i,d->name);
if(d->description){
//打印适配器的描述信息
printf("description:%s\n",d->description);
}else{
//适配器不存在描述信息
printf("description:%s","no description\n");
}
//打印本地环回地址
printf("\tLoopback: %s\n",(d->flags & PCAP_IF_LOOPBACK)?"yes":"no");
/**
pcap_addr * next 指向下一个地址的指针
sockaddr * addr IP地址
sockaddr * netmask 子网掩码
sockaddr * broadaddr 广播地址
sockaddr * dstaddr 目的地址
*/
pcap_addr_t *a; //网络适配器的地址用来存储变量
for(a = d->addresses;a;a = a->next){
//sa_family代表了地址的类型,是IPV4地址类型还是IPV6地址类型
switch (a->addr->sa_family)
{
case AF_INET: //代表IPV4类型地址
printf("Address Family Name:AF_INET\n");
if(a->addr){
//->的优先级等同于括号,高于强制类型转换,因为addr为sockaddr类型,对其进行操作须转换为sockaddr_in类型
printf("Address:%s\n",iptos(((struct sockaddr_in *)a->addr)->sin_addr.s_addr));
}
if (a->netmask){
printf("\tNetmask: %s\n",iptos(((struct sockaddr_in *)a->netmask)->sin_addr.s_addr));
}
if (a->broadaddr){
printf("\tBroadcast Address: %s\n",iptos(((struct sockaddr_in *)a->broadaddr)->sin_addr.s_addr));
}
if (a->dstaddr){
printf("\tDestination Address: %s\n",iptos(((struct sockaddr_in *)a->dstaddr)->sin_addr.s_addr));
}
break;
case AF_INET6: //代表IPV6类型地址
printf("Address Family Name:AF_INET6\n");
printf("this is an IPV6 address\n");
break;
default:
break;
}
}
}
//i为0代表上述循环未进入,即没有找到适配器,可能的原因为Winpcap没有安装导致未扫描到
if(i == 0){
printf("interface not found,please check winpcap installation");
}

int num;
printf("Enter the interface number(1-%d):",i);
//让用户选择选择哪个适配器进行抓包
scanf_s("%d",&num);
printf("\n");

//用户输入的数字超出合理范围
if(num<1||num>i){
printf("number out of range\n");
pcap_freealldevs(alldevs);
return -1;
}
//跳转到选中的适配器
for(d=alldevs, i=0; i< num-1 ; d=d->next, i++);

//运行到此处说明用户的输入是合法的
if((adhandle = pcap_open(d->name, //设备名称
65535, //存放数据包的内容长度
PCAP_OPENFLAG_PROMISCUOUS, //混杂模式
1000, //超时时间
NULL, //远程验证
errbuf //错误缓冲
)) == NULL){
//打开适配器失败,打印错误并释放适配器列表
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
// 释放设备列表
pcap_freealldevs(alldevs);
return -1;
}

上述代码中需要另外声明的有:

1
2
3
4
5
pcap_if_t  * alldevs;       //所有网络适配器
pcap_if_t *d; //选中的网络适配器
char errbuf[PCAP_ERRBUF_SIZE]; //错误缓冲区,大小为256
pcap_t *adhandle; //捕捉实例,是pcap_open返回的对象
int i = 0; //适配器计数变量
1
char *iptos(u_long in);       //u_long即为 unsigned long
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 将数字类型的IP地址转换成字符串类型的 */
#define IPTOSBUFFERS 12
char *iptos(u_long in)
{
static char output[IPTOSBUFFERS][3*4+3+1];
static short which;
u_char *p;

p = (u_char *)&in;
which = (which + 1 == IPTOSBUFFERS ? 0 : which + 1);
sprintf_s(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
return output[which];
}

到此程序应该会编译通过,可以试着编译一下运行。 GO ON… 接下来我们首先要用ifget方法获取自身的IP和子网掩码 函数声明:

1
void ifget(pcap_if_t *d, char *ip_addr, char *ip_netmask);
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
//获取IP和子网掩码赋值为ip_addr和ip_netmask
void ifget(pcap_if_t *d, char *ip_addr, char *ip_netmask) {
pcap_addr_t *a;
//遍历所有的地址,a代表一个pcap_addr
for (a = d->addresses; a; a = a->next) {
switch (a->addr->sa_family) {
case AF_INET: //sa_family :是2字节的地址家族,一般都是“AF_xxx”的形式。通常用的都是AF_INET。代表IPV4
if (a->addr) {
char *ipstr;
//将地址转化为字符串
ipstr = iptos(((struct sockaddr_in *) a->addr)->sin_addr.s_addr); //*ip_addr
printf("ipstr:%s\n",ipstr);
memcpy(ip_addr, ipstr, 16);
}
if (a->netmask) {
char *netmaskstr;
netmaskstr = iptos(((struct sockaddr_in *) a->netmask)->sin_addr.s_addr);
printf("netmask:%s\n",netmaskstr);
memcpy(ip_netmask, netmaskstr, 16);
}
case AF_INET6:
break;
}
}
}

main函数继续写,如下调用,之前声明的ip_addr和ip_netmask就已经被赋值了

1
ifget(d, ip_addr, ip_netmask); //获取所选网卡的基本信息--掩码--IP地址

到现在我们已经获取到了本机的IP和子网掩码,下一步发送一个ARP请求来获取自身的MAC地址 这个ARP请求的源IP地址就随便指定了,就相当于你构造了一个外来的ARP请求,本机捕获到了请求,然后发送回应给对方的数据包也被本机捕获到了并解析出来了。解析了自己发出去的数据包而已。 那么我们就声明一个函数并实现:

1
int GetSelfMac(pcap_t *adhandle, const char *ip_addr, unsigned char *ip_mac);
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
// 获取自己主机的MAC地址
int GetSelfMac(pcap_t *adhandle, const char *ip_addr, unsigned char *ip_mac) {
unsigned char sendbuf[42]; //arp包结构大小
int i = -1;
int res;
EthernetHeader eh; //以太网帧头
Arpheader ah; //ARP帧头
struct pcap_pkthdr * pkt_header;
const u_char * pkt_data;
//将已开辟内存空间 eh.dest_mac_add 的首 6个字节的值设为值 0xff。
memset(eh.DestMAC, 0xff, 6); //目的地址为全为广播地址
memset(eh.SourMAC, 0x0f, 6);
memset(ah.DestMacAdd, 0x0f, 6);
memset(ah.SourceMacAdd, 0x00, 6);
//htons将一个无符号短整型的主机数值转换为网络字节顺序
eh.EthType = htons(ETH_ARP);
ah.HardwareType= htons(ARP_HARDWARE);
ah.ProtocolType = htons(ETH_IP);
ah.HardwareAddLen = 6;
ah.ProtocolAddLen = 4;
ah.SourceIpAdd = inet_addr("100.100.100.100"); //随便设的请求方ip
ah.OperationField = htons(ARP_REQUEST);
ah.DestIpAdd = inet_addr(ip_addr);
memset(sendbuf, 0, sizeof(sendbuf));
memcpy(sendbuf, &eh, sizeof(eh));
memcpy(sendbuf + sizeof(eh), &ah, sizeof(ah));
printf("%s",sendbuf);
if (pcap_sendpacket(adhandle, sendbuf, 42) == 0) {
printf("\nPacketSend succeed\n");
} else {
printf("PacketSendPacket in getmine Error: %d\n", GetLastError());
return 0;
}
//从interface或离线记录文件获取一个报文
//pcap_next_ex(pcap_t* p,struct pcap_pkthdr** pkt_header,const u_char** pkt_data)
while ((res = pcap_next_ex(adhandle, &pkt_header, &pkt_data)) >= 0) {
if (*(unsigned short *) (pkt_data + 12) == htons(ETH_ARP)
&& *(unsigned short*) (pkt_data + 20) == htons(ARP_REPLY)
&& *(unsigned long*) (pkt_data + 38)
== inet_addr("100.100.100.100")) {
for (i = 0; i < 6; i++) {
ip_mac[i] = *(unsigned char *) (pkt_data + 22 + i);
}
printf("获取自己主机的MAC地址成功!\n");
break;
}
}
if (i == 6) {
return 1;
} else {
return 0;
}
}

其中我们需要定义一下常量如下

1
2
3
4
5
6
#define ETH_ARP         0x0806  //以太网帧类型表示后面数据的类型,对于ARP请求或应答来说,该字段的值为x0806
#define ARP_HARDWARE 1 //硬件类型字段值为表示以太网地址
#define ETH_IP 0x0800 //协议类型字段表示要映射的协议地址类型值为x0800表示IP地址
#define ARP_REQUEST 1 //ARP请求
#define ARP_REPLY 2 //ARP应答
#define HOSTNUM 255 //主机数量

另外发送ARP请求少不了帧头和ARP头的结构,我们需要声明出来,另外我们构建发送包需要再声明两个结构体sparam和gparam

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
//帧头部结构体,共14字节
struct EthernetHeader
{
u_char DestMAC[6]; //目的MAC地址 6字节
u_char SourMAC[6]; //源MAC地址 6字节
u_short EthType; //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp 2字节
};

//28字节ARP帧结构
struct Arpheader {
unsigned short HardwareType; //硬件类型
unsigned short ProtocolType; //协议类型
unsigned char HardwareAddLen; //硬件地址长度
unsigned char ProtocolAddLen; //协议地址长度
unsigned short OperationField; //操作字段
unsigned char SourceMacAdd[6]; //源mac地址
unsigned long SourceIpAdd; //源ip地址
unsigned char DestMacAdd[6]; //目的mac地址
unsigned long DestIpAdd; //目的ip地址
};

//arp包结构
struct ArpPacket {
EthernetHeader ed;
Arpheader ah;
};

struct sparam {
pcap_t *adhandle;
char *ip;
unsigned char *mac;
char *netmask;
};
struct gparam {
pcap_t *adhandle;
};

struct sparam sp;
struct gparam gp;

到现在代码也是完整可以运行的,如果有问题请检查上述代码完整性和位置。 可能出现的BUG: 只显示ARP发送成功,没有接受到并解析打印。可能的原因是帧构造有问题,字节没有对齐,有偏差,像#define一样 写入如下代码:

1
#pragma pack(1)  //按一个字节内存对齐

GO ON.. 获取到了自身的MAC地址之后,就可以在本机上构建ARP广播请求,向局域网内的所有主机发送ARP请求,得到回应之后解析回应的数据包并进行解析,得到对方的MAC地址。在这里我们需要开启两个线程,一个用来发送一个用来接收。好,我们继续.. 先声明两个线程

1
2
HANDLE sendthread;      //发送ARP包线程
HANDLE recvthread; //接受ARP包线程

在main方法中继续写,对sp和gp两个ARP请求所需要的结构体进行赋值。赋值什么?就是你之前用ifget获取来的IP地址和子网掩码以及用getSelfMac获取来的MAC地址。

1
2
3
4
5
sp.adhandle = adhandle;
sp.ip = ip_addr;
sp.mac = ip_mac;
sp.netmask = ip_netmask;
gp.adhandle = adhandle;

接下来直接创建两个线程,一个是发送一个接受,分别调用两个方法。

1
2
3
4
5
sendthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) SendArpPacket,
&sp, 0, NULL);
recvthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) GetLivePC, &gp,
0, NULL);
printf("\nlistening on 网卡%d ...\n", i);

那么发送数据包的方法和接收解析数据包的方法怎样实现呢? 发送数据包,发送数据包先对结构体数据进行赋值,就像getSelfMac方法一样,然后声明了一个buffer用来存储每一个字节内容。 利用memset方法对buffer进行赋值。再利用一个for循环对255个主机进行发送,指定他们的IP地址。另外定义了一个flag,当发送成功之后将flag设置为1

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
/* 向局域网内所有可能的IP地址发送ARP请求包线程 */
DWORD WINAPI SendArpPacket(LPVOID lpParameter) //(pcap_t *adhandle,char *ip,unsigned char *mac,char *netmask)
{
sparam *spara = (sparam *) lpParameter;
pcap_t *adhandle = spara->adhandle;
char *ip = spara->ip;
unsigned char *mac = spara->mac;
char *netmask = spara->netmask;
printf("ip_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", mac[0], mac[1], mac[2],
mac[3], mac[4], mac[5]);
printf("自身的IP地址为:%s\n", ip);
printf("地址掩码NETMASK为:%s\n", netmask);
printf("\n");
unsigned char sendbuf[42]; //arp包结构大小
EthernetHeader eh;
Arpheader ah;
//赋值MAC地址
memset(eh.DestMAC, 0xff, 6); //目的地址为全为广播地址
memcpy(eh.SourMAC, mac, 6);
memcpy(ah.SourceMacAdd, mac, 6);
memset(ah.DestMacAdd, 0x00, 6);
eh.EthType = htons(ETH_ARP);
ah.HardwareType = htons(ARP_HARDWARE);
ah.ProtocolType = htons(ETH_IP);
ah.HardwareAddLen = 6;
ah.ProtocolAddLen = 4;
ah.SourceIpAdd = inet_addr(ip); //请求方的IP地址为自身的IP地址
ah.OperationField = htons(ARP_REQUEST);
//向局域网内广播发送arp包
unsigned long myip = inet_addr(ip);
unsigned long mynetmask = inet_addr(netmask);
unsigned long hisip = htonl((myip & mynetmask));
//向255个主机发送
for (int i = 0; i < HOSTNUM; i++) {
ah.DestIpAdd = htonl(hisip + i);
//构造一个ARP请求
memset(sendbuf, 0, sizeof(sendbuf));
memcpy(sendbuf, &eh, sizeof(eh));
memcpy(sendbuf + sizeof(eh), &ah, sizeof(ah));
//如果发送成功
if (pcap_sendpacket(adhandle, sendbuf, 42) == 0) {
//printf("\nPacketSend succeed\n");
} else {
printf("PacketSendPacket in getmine Error: %d\n", GetLastError());
}
Sleep(50);
}
Sleep(1000);
flag = TRUE;
return 0;
}

注: 此函数和flag变量在前面别忘了声明一下… 然后是接收数据包并打印MAC地址:

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
/* 分析截留的数据包获取活动的主机IP地址 */
DWORD WINAPI GetLivePC(LPVOID lpParameter) //(pcap_t *adhandle)
{
gparam *gpara = (gparam *) lpParameter;
pcap_t *adhandle = gpara->adhandle;
int res;
unsigned char Mac[6];
struct pcap_pkthdr * pkt_header;
const u_char * pkt_data;
while (true) {
if (flag) {
printf("获取MAC地址完毕,请输入你要发送对方的IP地址:\n");
break;
}
if ((res = pcap_next_ex(adhandle, &pkt_header, &pkt_data)) >= 0) {
if (*(unsigned short *) (pkt_data + 12) == htons(ETH_ARP)) {
ArpPacket *recv = (ArpPacket *) pkt_data;
if (*(unsigned short *) (pkt_data + 20) == htons(ARP_REPLY)) {
printf("-------------------------------------------\n");
printf("IP地址:%d.%d.%d.%d MAC地址:",
recv->ah.SourceIpAdd & 255,
recv->ah.SourceIpAdd >> 8 & 255,
recv->ah.SourceIpAdd >> 16 & 255,
recv->ah.SourceIpAdd >> 24 & 255);
for (int i = 0; i < 6; i++) {
Mac[i] = *(unsigned char *) (pkt_data + 22 + i);
printf("%02x", Mac[i]);
}
printf("\n");
}
}
}
Sleep(10);
}
return 0;
}

以上暂告一段落,通过整合以上代码,我们可以得到如下运行结果:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
--------------------------------------------------
number:1
name:rpcap://\Device\NPF_{5AC72F8D-019C-4003-B51B-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET6
this is an IPV6 address
--------------------------------------------------
number:2
name:rpcap://\Device\NPF_{C17EB3F6-1E86-40E5-8790-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET
Address:192.168.95.1
Netmask: 255.255.255.0
Broadcast Address: 255.255.255.255
--------------------------------------------------
number:3
name:rpcap://\Device\NPF_{33E23A2F-F791-409B-8452-
description:Network adapter 'Qualcomm Atheros Ar81
oller' on local host
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET
Address:121.250.216.237
Netmask: 255.255.255.0
Broadcast Address: 255.255.255.255
--------------------------------------------------
number:4
name:rpcap://\Device\NPF_{DCCF036F-A9A8-4225-B980-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET6
this is an IPV6 address
--------------------------------------------------
number:5
name:rpcap://\Device\NPF_{D62A0060-F424-46FC-83A5-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET
Address:192.168.191.1
Netmask: 255.255.255.0
Broadcast Address: 255.255.255.255
--------------------------------------------------
number:6
name:rpcap://\Device\NPF_{B5224A53-8450-4537-AB3B-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET
Address:192.168.191.2
Netmask: 255.255.255.0
Broadcast Address: 255.255.255.255
Enter the interface number(1-6):3

ipstr:121.250.216.237
netmask:255.255.255.0

PacketSend succeed
获取自己主机的MAC地址成功!

listening on 网卡2 ...
ip_mac:dc-0e-a1-ec-53-c3
自身的IP地址为:121.250.216.237
地址掩码NETMASK为:255.255.255.0

请按任意键继续. . . ------------------------------
IP地址:121.250.216.1 MAC地址:000fe28e6100
-------------------------------------------
IP地址:121.250.216.3 MAC地址:089e012d20d5
-------------------------------------------
IP地址:121.250.216.5 MAC地址:5404a6af5f2d
-------------------------------------------
IP地址:121.250.216.6 MAC地址:28d244248d81
-------------------------------------------
IP地址:121.250.216.7 MAC地址:80fa5b0283f3
-------------------------------------------
IP地址:121.250.216.8 MAC地址:14dae9005b9e
-------------------------------------------
IP地址:121.250.216.9 MAC地址:b82a72bf8bce
-------------------------------------------
IP地址:121.250.216.12 MAC地址:84c9b2fefeed
-------------------------------------------
IP地址:121.250.216.15 MAC地址:28d2440b4b1b
-------------------------------------------
IP地址:121.250.216.16 MAC地址:bcee7b969beb
-------------------------------------------
........此处省略一万字....

接下来我们让用户输入要发送的IP地址和要发送的数据

1
2
3
4
5
6
u_int ip1,ip2,ip3,ip4;
scanf_s("%d.%d.%d.%d",&ip1,&ip2,&ip3,&ip4);
printf("请输入你要发送的内容:\n");
getchar();
gets_s(TcpData);
printf("要发送的内容:%s\n",TcpData);

声明一下TcpData

1
char TcpData[20];   //发送内容

接下来就是重头戏了,需要声明各种结构体,我们发送的是TCP数据,这样,TCP的TcpData 就作为真正的内容,然后在前面加上TCP头,IP头,帧头,还有校验和要正确。 最后构成一个完整的帧,那么另外声明的结构体如下,前面代码声明过的帧头部结构体就去掉了。

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
54
55
//IP地址格式
struct IpAddress
{
u_char byte1;
u_char byte2;
u_char byte3;
u_char byte4;
};

//帧头部结构体,共14字节
struct EthernetHeader
{
u_char DestMAC[6]; //目的MAC地址 6字节
u_char SourMAC[6]; //源MAC地址 6字节
u_short EthType; //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp 2字节
};

//IP头部结构体,共20字节
struct IpHeader
{
unsigned char Version_HLen; //版本信息4位 ,头长度4位 1字节
unsigned char TOS; //服务类型 1字节
short Length; //数据包长度 2字节
short Ident; //数据包标识 2字节
short Flags_Offset; //标志3位,片偏移13位 2字节
unsigned char TTL; //存活时间 1字节
unsigned char Protocol; //协议类型 1字节
short Checksum; //首部校验和 2字节
IpAddress SourceAddr; //源IP地址 4字节
IpAddress DestinationAddr; //目的IP地址 4字节
};

//TCP头部结构体,共20字节
struct TcpHeader
{
unsigned short SrcPort; //源端口号 2字节
unsigned short DstPort; //目的端口号 2字节
unsigned int SequenceNum; //序号 4字节
unsigned int Acknowledgment; //确认号 4字节
unsigned char HdrLen; //首部长度4位,保留位6位 共10位
unsigned char Flags; //标志位6位
unsigned short AdvertisedWindow; //窗口大小16位 2字节
unsigned short Checksum; //校验和16位 2字节
unsigned short UrgPtr; //紧急指针16位 2字节
};

//TCP伪首部结构体 12字节
struct PsdTcpHeader
{
IpAddress SourceAddr; //源IP地址 4字节
IpAddress DestinationAddr; //目的IP地址 4字节
char Zero; //填充位 1字节
char Protcol; //协议号 1字节
unsigned short TcpLen; //TCP包长度 2字节
};

继续main函数中对各种结构体的数据进行初始化赋值,并计算校验和。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//结构体初始化为0序列
memset(&ethernet, 0, sizeof(ethernet));
BYTE destmac[8];
//目的MAC地址,此处没有对帧的MAC地址进行赋值,因为网卡设置的混杂模式,可以接受经过该网卡的所有帧。当然最好的方法是赋值为ARP刚才获取到的MAC地址,当然不赋值也可以捕捉到并解析,在此处仅做下说明。
destmac[0] = 0x00;
destmac[1] = 0x11;
destmac[2] = 0x22;
destmac[3] = 0x33;
destmac[4] = 0x44;
destmac[5] = 0x55;
//赋值目的MAC地址
memcpy(ethernet.DestMAC, destmac, 6);
BYTE hostmac[8];
//源MAC地址
hostmac[0] = 0x00;
hostmac[1] = 0x1a;
hostmac[2] = 0x4d;
hostmac[3] = 0x70;
hostmac[4] = 0xa3;
hostmac[5] = 0x89;
//赋值源MAC地址
memcpy(ethernet.SourMAC, hostmac, 6);
//上层协议类型,0x0800代表IP协议
ethernet.EthType = htons(0x0800);
//赋值SendBuffer
memcpy(&SendBuffer, ðernet, sizeof(struct EthernetHeader));
//赋值IP头部信息
ip.Version_HLen = 0x45;
ip.TOS = 0;
ip.Length = htons(sizeof(struct IpHeader) + sizeof(struct TcpHeader) + strlen(TcpData));
ip.Ident = htons(1);
ip.Flags_Offset = 0;
ip.TTL = 128;
ip.Protocol = 6;
ip.Checksum = 0;
//源IP地址
ip.SourceAddr.byte1 = 127;
ip.SourceAddr.byte2 = 0;
ip.SourceAddr.byte3 = 0;
ip.SourceAddr.byte4 = 1;
//目的IP地址
ip.DestinationAddr.byte1 = ip1;
ip.DestinationAddr.byte2 = ip2;
ip.DestinationAddr.byte3 = ip3;
ip.DestinationAddr.byte4 = ip4;
//赋值SendBuffer
memcpy(&SendBuffer[sizeof(struct EthernetHeader)], &ip, 20);
//赋值TCP头部内容
tcp.DstPort = htons(102);
tcp.SrcPort = htons(1000);
tcp.SequenceNum = htonl(11);
tcp.Acknowledgment = 0;
tcp.HdrLen = 0x50;
tcp.Flags = 0x18;
tcp.AdvertisedWindow = htons(512);
tcp.UrgPtr = 0;
tcp.Checksum = 0;
//赋值SendBuffer
memcpy(&SendBuffer[sizeof(struct EthernetHeader) + 20], &tcp, 20);
//赋值伪首部
ptcp.SourceAddr = ip.SourceAddr;
ptcp.DestinationAddr = ip.DestinationAddr;
ptcp.Zero = 0;
ptcp.Protcol = 6;
ptcp.TcpLen = htons(sizeof(struct TcpHeader) + strlen(TcpData));
//声明临时存储变量,用来计算校验和
char TempBuffer[65535];
memcpy(TempBuffer, &ptcp, sizeof(struct PsdTcpHeader));
memcpy(TempBuffer + sizeof(struct PsdTcpHeader), &tcp, sizeof(struct TcpHeader));
memcpy(TempBuffer + sizeof(struct PsdTcpHeader) + sizeof(struct TcpHeader), TcpData, strlen(TcpData));
//计算TCP的校验和
tcp.Checksum = checksum((USHORT*)(TempBuffer), sizeof(struct PsdTcpHeader) + sizeof(struct TcpHeader) + strlen(TcpData));
//重新把SendBuffer赋值,因为此时校验和已经改变,赋值新的
memcpy(SendBuffer + sizeof(struct EthernetHeader) + sizeof(struct IpHeader), &tcp, sizeof(struct TcpHeader));
memcpy(SendBuffer + sizeof(struct EthernetHeader) + sizeof(struct IpHeader) + sizeof(struct TcpHeader), TcpData, strlen(TcpData));
//初始化TempBuffer为0序列,存储变量来计算IP校验和
memset(TempBuffer, 0, sizeof(TempBuffer));
memcpy(TempBuffer, &ip, sizeof(struct IpHeader));
//计算IP校验和
ip.Checksum = checksum((USHORT*)(TempBuffer), sizeof(struct IpHeader));
//重新把SendBuffer赋值,IP校验和已经改变
memcpy(SendBuffer + sizeof(struct EthernetHeader), &ip, sizeof(struct IpHeader));
//发送序列的长度
int size = sizeof(struct EthernetHeader) + sizeof(struct IpHeader) + sizeof(struct TcpHeader) + strlen(TcpData);
int result = pcap_sendpacket(adhandle, SendBuffer,size);
if (result != 0)
{
printf("Send Error!\n");
}
else
{
printf("Send TCP Packet.\n");
printf("Dstination Port:%d\n", ntohs(tcp.DstPort));
printf("Source Port:%d\n", ntohs(tcp.SrcPort));
printf("Sequence:%d\n", ntohl(tcp.SequenceNum));
printf("Acknowledgment:%d\n", ntohl(tcp.Acknowledgment));
printf("Header Length:%d*4\n", tcp.HdrLen >> 4);
printf("Flags:0x%0x\n", tcp.Flags);
printf("AdvertiseWindow:%d\n", ntohs(tcp.AdvertisedWindow));
printf("UrgPtr:%d\n", ntohs(tcp.UrgPtr));
printf("Checksum:%u\n", ntohs(tcp.Checksum));
printf("Send Successfully!\n");
}

校验和方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获得校验和的方法
unsigned short checksum(unsigned short *data, int length)
{
unsigned long temp = 0;
while (length > 1)
{
temp += *data++;
length -= sizeof(unsigned short);
}
if (length)
{
temp += *(unsigned short*)data;
}
temp = (temp >> 16) + (temp &0xffff);
temp += (temp >> 16);
return (unsigned short)(~temp);
}

记得在声明一下这个方法。如果放在main函数前当然就不用声明啦。 另外需要声明的变量有

1
2
3
4
struct EthernetHeader ethernet;    //以太网帧头
struct IpHeader ip; //IP头
struct TcpHeader tcp; //TCP头
struct PsdTcpHeader ptcp; //TCP伪首部
1
unsigned char SendBuffer[200];       //发送队列

接下来的运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
获取MAC地址完毕,请输
121.250.216.112
请输入你要发送的内容
what is tcp
要发送的内容:what i
Send TCP Packet.
Dstination Port:102
Source Port:1000
Sequence:11
Acknowledgment:0
Header Length:5*4
Flags:0x18
AdvertiseWindow:512
UrgPtr:0
Checksum:17149
Send Successfully!

截图如下: 好啦,发送帧到此就告一段落啦!如果有疑问请留言。 帧的接收很简单,直接贴源码如下:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>


char *iptos(u_long in); //u_long即为 unsigned long
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data);
//struct tm *ltime; //和时间处理有关的变量

struct IpAddress
{
u_char byte1;
u_char byte2;
u_char byte3;
u_char byte4;
};

//帧头部结构体,共14字节
struct EthernetHeader
{
u_char DestMAC[6]; //目的MAC地址 6字节
u_char SourMAC[6]; //源MAC地址 6字节
u_short EthType; //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp 2字节
};

//IP头部结构体,共20字节
struct IpHeader
{
unsigned char Version_HLen; //版本信息4位 ,头长度4位 1字节
unsigned char TOS; //服务类型 1字节
short Length; //数据包长度 2字节
short Ident; //数据包标识 2字节
short Flags_Offset; //标志3位,片偏移13位 2字节
unsigned char TTL; //存活时间 1字节
unsigned char Protocol; //协议类型 1字节
short Checksum; //首部校验和 2字节
IpAddress SourceAddr; //源IP地址 4字节
IpAddress DestinationAddr; //目的IP地址 4字节
};

//TCP头部结构体,共20字节
struct TcpHeader
{
unsigned short SrcPort; //源端口号 2字节
unsigned short DstPort; //目的端口号 2字节
unsigned int SequenceNum; //序号 4字节
unsigned int Acknowledgment; //确认号 4字节
unsigned char HdrLen; //首部长度4位,保留位6位 共10位
unsigned char Flags; //标志位6位
unsigned short AdvertisedWindow; //窗口大小16位 2字节
unsigned short Checksum; //校验和16位 2字节
unsigned short UrgPtr; //紧急指针16位 2字节
};

//TCP伪首部结构体 12字节
struct PsdTcpHeader
{
unsigned long SourceAddr; //源IP地址 4字节
unsigned long DestinationAddr; //目的IP地址 4字节
char Zero; //填充位 1字节
char Protcol; //协议号 1字节
unsigned short TcpLen; //TCP包长度 2字节
};


int main(){

EthernetHeader *ethernet; //以太网帧头
IpHeader *ip; //IP头
TcpHeader *tcp; //TCP头
PsdTcpHeader *ptcp; //TCP伪首部

pcap_if_t * alldevs; //所有网络适配器
pcap_if_t *d; //选中的网络适配器
char errbuf[PCAP_ERRBUF_SIZE]; //错误缓冲区,大小为256
char source[PCAP_ERRBUF_SIZE];
pcap_t *adhandle; //捕捉实例,是pcap_open返回的对象
int i = 0; //适配器计数变量
struct pcap_pkthdr *header; //接收到的数据包的头部
const u_char *pkt_data; //接收到的数据包的内容
int res; //表示是否接收到了数据包
u_int netmask; //过滤时用的子网掩码
char packet_filter[] = "tcp"; //过滤字符
struct bpf_program fcode; //pcap_compile所调用的结构体

u_int ip_len; //ip地址有效长度
u_short sport,dport; //主机字节序列
u_char packet[100]; //发送数据包目的地址
pcap_dumper_t *dumpfile; //堆文件

//time_t local_tv_sec; //和时间处理有关的变量
//char timestr[16]; //和时间处理有关的变量


//获取本地适配器列表
if(pcap_findalldevs_ex(PCAP_SRC_IF_STRING,NULL,&alldevs,errbuf) == -1){
//结果为-1代表出现获取适配器列表失败
fprintf(stderr,"Error in pcap_findalldevs_ex:\n",errbuf);
//exit(0)代表正常退出,exit(other)为非正常退出,这个值会传给操作系统
exit(1);
}
//打印设备列表信息
for(d = alldevs;d !=NULL;d = d->next){
printf("-----------------------------------------------------------------\nnumber:%d\nname:%s\n",++i,d->name);
if(d->description){
//打印适配器的描述信息
printf("description:%s\n",d->description);
}else{
//适配器不存在描述信息
printf("description:%s","no description\n");
}
//打印本地环回地址
printf("\tLoopback: %s\n",(d->flags & PCAP_IF_LOOPBACK)?"yes":"no");

pcap_addr_t *a; //网络适配器的地址用来存储变量
for(a = d->addresses;a;a = a->next){
//sa_family代表了地址的类型,是IPV4地址类型还是IPV6地址类型
switch (a->addr->sa_family)
{
case AF_INET: //代表IPV4类型地址
printf("Address Family Name:AF_INET\n");
if(a->addr){
//->的优先级等同于括号,高于强制类型转换,因为addr为sockaddr类型,对其进行操作须转换为sockaddr_in类型
printf("Address:%s\n",iptos(((struct sockaddr_in *)a->addr)->sin_addr.s_addr));
}
if (a->netmask){
printf("\tNetmask: %s\n",iptos(((struct sockaddr_in *)a->netmask)->sin_addr.s_addr));
}
if (a->broadaddr){
printf("\tBroadcast Address: %s\n",iptos(((struct sockaddr_in *)a->broadaddr)->sin_addr.s_addr));
}
if (a->dstaddr){
printf("\tDestination Address: %s\n",iptos(((struct sockaddr_in *)a->dstaddr)->sin_addr.s_addr));
}
break;
case AF_INET6: //代表IPV6类型地址
printf("Address Family Name:AF_INET6\n");
printf("this is an IPV6 address\n");
break;
default:
break;
}
}
}
//i为0代表上述循环未进入,即没有找到适配器,可能的原因为Winpcap没有安装导致未扫描到
if(i == 0){
printf("interface not found,please check winpcap installation");
}

int num;
printf("Enter the interface number(1-%d):",i);
//让用户选择选择哪个适配器进行抓包
scanf_s("%d",&num);
printf("\n");

//用户输入的数字超出合理范围
if(num<1||num>i){
printf("number out of range\n");
pcap_freealldevs(alldevs);
return -1;
}
//跳转到选中的适配器
for(d=alldevs, i=0; i< num-1 ; d=d->next, i++);

//运行到此处说明用户的输入是合法的
if((adhandle = pcap_open(d->name, //设备名称
65535, //存放数据包的内容长度
PCAP_OPENFLAG_PROMISCUOUS, //混杂模式
1000, //超时时间
NULL, //远程验证
errbuf //错误缓冲
)) == NULL){
//打开适配器失败,打印错误并释放适配器列表
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
// 释放设备列表
pcap_freealldevs(alldevs);
return -1;
}


//打印输出,正在监听中
printf("\nlistening on %s...\n", d->description);

//所在网络不是以太网,此处只取这种情况
if(pcap_datalink(adhandle) != DLT_EN10MB)
{
fprintf(stderr,"\nThis program works only on Ethernet networks.\n");
//释放列表
pcap_freealldevs(alldevs);
return -1;
}

//先获得地址的子网掩码
if(d->addresses != NULL)
//获得接口第一个地址的掩码
netmask=((struct sockaddr_in *)(d->addresses->netmask))->sin_addr.S_un.S_addr;
else
// 如果接口没有地址,那么我们假设一个C类的掩码
netmask=0xffffff;

//pcap_compile()的原理是将高层的布尔过滤表
//达式编译成能够被过滤引擎所解释的低层的字节码
if(pcap_compile(adhandle, //适配器处理对象
&fcode,
packet_filter, //过滤ip和UDP
1, //优化标志
netmask //子网掩码
)<0)
{
//过滤出现问题
fprintf(stderr,"\nUnable to compile the packet filter. Check the syntax.\n");
// 释放设备列表
pcap_freealldevs(alldevs);
return -1;
}

//设置过滤器
if (pcap_setfilter(adhandle, &fcode)<0)
{
fprintf(stderr,"\nError setting the filter.\n");
//释放设备列表
pcap_freealldevs(alldevs);
return -1;
}


//利用pcap_next_ex来接受数据包
while((res = pcap_next_ex(adhandle,&header,&pkt_data))>=0)
{
if(res ==0){
//返回值为0代表接受数据包超时,重新循环继续接收
continue;
}else{
//运行到此处代表接受到正常从数据包
//header为帧的头部
printf("%.6ld len:%d ", header->ts.tv_usec, header->len);
// 获得IP数据包头部的位置
ip = (IpHeader *) (pkt_data +14); //14为以太网帧头部长度
//获得TCP头部的位置
ip_len = (ip->Version_HLen & 0xf) *4;
printf("ip_length:%d ",ip_len);
tcp = (TcpHeader *)((u_char *)ip+ip_len);
char * data;
data = (char *)((u_char *)tcp+20);
//将网络字节序列转换成主机字节序列
sport = ntohs( tcp->SrcPort );
dport = ntohs( tcp->DstPort );
printf("srcport:%d desport:%d\n",sport,dport);
printf("%d.%d.%d.%d.%d -> %d.%d.%d.%d.%d\n",
ip->SourceAddr.byte1,
ip->SourceAddr.byte2,
ip->SourceAddr.byte3,
ip->SourceAddr.byte4,
sport,
ip->DestinationAddr.byte1,
ip->DestinationAddr.byte2,
ip->DestinationAddr.byte3,
ip->DestinationAddr.byte4,
dport);
printf("%s\n",data);
}

}


//释放网络适配器列表
pcap_freealldevs(alldevs);

/**
int pcap_loop ( pcap_t * p,
int cnt,
pcap_handler callback,
u_char * user
);
typedef void (*pcap_handler)(u_char *, const struct pcap_pkthdr *,
const u_char *);
*/
//开始捕获信息,当捕获到数据包时,会自动调用这个函数
//pcap_loop(adhandle,0,packet_handler,NULL);

int inum;
scanf_s("%d", &inum);

return 0;

}

/* 每次捕获到数据包时,libpcap都会自动调用这个回调函数 */
/**
pcap_loop()函数是基于回调的原理来进行数据捕获的,如技术文档所说,这是一种精妙的方法,并且在某些场合下,
它是一种很好的选择。但是在处理回调有时候会并不实用,它会增加程序的复杂度,特别是在多线程的C++程序中
*/
/*
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data)
{
struct tm *ltime = NULL;
char timestr[16];
time_t local_tv_sec;

// 将时间戳转换成可识别的格式
local_tv_sec = header->ts.tv_sec;
localtime_s(ltime,&local_tv_sec);
strftime( timestr, sizeof timestr, "%H:%M:%S", ltime);

printf("%s,%.6ld len:%d\n", timestr, header->ts.tv_usec, header->len);

}
*/
/* 将数字类型的IP地址转换成字符串类型的 */
#define IPTOSBUFFERS 12
char *iptos(u_long in)
{
static char output[IPTOSBUFFERS][3*4+3+1];
static short which;
u_char *p;

p = (u_char *)&in;
which = (which + 1 == IPTOSBUFFERS ? 0 : which + 1);
sprintf_s(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
return output[which];
}

运行截图如下 Thank You 如有问题,欢迎留言~

C/C++

上一篇我们介绍了Eclipse配置Winpcap环境。但是美中不足的是Eclipse编写Winpcap会出现各种各样神奇的错误… 本文原文地址: 我的CSDN博客 其实也不是不能用,就是总是报一些函数不能识别的错误,每次不能识别的函数就是因为缺少了各种各样的头文件或者宏定义…让我比较纠结,心中不由得萌生了一个想法,换个环境试试吧,于是VS环境配置就这么神奇的产生了…. 好,接下来我以VS2012为例为大家讲解Winpcap环境配置,其实VS2010,VS2013等等的版本都是通用哒.. 首先你要有一个VS…这不是废话么… 首先,按照 Eclipse配置Winpcap环境 配置好Winpcap和下载好相关的SDK,这里就不再赘述了 好,文件-新建-项目-Win32控制台应用程序,可以修改下面的路径和项目名称,然后确定 接下来就会蹦出一个窗口..点击下一步 下一步,勾选空项目,然后点击确定,空项目一定要勾选,不然会生成一些不必要的文件。 点击完成,现在看到的界面如下: 在项目名上右键单击,选择属性 链接器选项卡—输入—附加依赖项中在前面输入ws2_32.lib;wpcap.lib; 两个类库,截图如下 C/C++选项卡—-预处理器—-预处理器定义键入HAVE_REMOTE;WPCAP; 如图所示 VC++目录,加入你的Winpcap类库路径,文件夹应该叫WpdPack,一个是Include,一个是lib,这是绝对路径,你的可能与我的不一样,截图如下 好了。点击确定,环境就配好啦。 接下来新建一个cpp文件,运行程序就OK了… 如有问题,欢迎留言交流

C/C++

大家好,本节为大家带来在Eclipse下配置Winpcap环境,欢迎大家收看。 本文原文地址来自: 我的CSDN博客 首先,配置Winpcap环境的前提是你必须配置好了Eclipse下的C/C++环境。如果你还没有配置,欢迎大家收看上节内容进行配置。 链接地址: Eclipse配置C/C++环境 若链接失效,请自行查看上一篇文章或者百度其他文章。 废话不多说啦,开始我们的Winpcap的配置。

Winpcap下载

Winpcap官网: Winpcap官网 Winpcap目前最新版为4.1.3,首先你要下载exe文件并安装,直接双击运行安装即可。 下载地址: Winpcap4.1.3.exe 然后你需要下载开发包,首先必须注意的是,目前最新版本是没有开发包的,最新的开发包为4.1.2,先见下图 下面的红色框说明了目前没有提供Winpcap4.1.3的开发包,最新版本的开发包是4.1.2,他可以与4.1.3的Winpcap配套使用。 所以下载4.1.2的开发包。 下载地址: Winpcap Developer’s Pack 4.1.2

Eclipse相关配置

首先新建一个C 的项目,具体的建立过程可以参见上一节的内容。 我们加入一个测试代码如下:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#define WIN32
#define HAVE_REMOTE

#include <stdio.h>
#include "pcap.h"
#include "Win32-Extensions.h"

void gen_packet(unsigned char *buf,int len);

/*生成数据包*/
void gen_packet(unsigned char *buf,int len)
{
int i=0;

//设置目标MAC地址为01:01:01:01:01:01
for (i=0;i<6;i++)
{
buf[i]=0x01;
}

//设置源MAC地址为02:02:02:02:02:02
for (i=6;i<12;i++)
{
buf[i]=0x02;
}

//设置协议标识为0xc0xd,无任何实际意义
buf[12]=0xc;
buf[13]=0xd;

//填充数据包的内容
for(i=14;i<len;i++)
{
buf[i]=i-14;
}
}


int main()
{
pcap_if_t *alldevs;
pcap_if_t *d;
int inum;
int i=0;
pcap_t *adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
int ret=-1;

/* 获取本机网络设备列表 */
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
{
fprintf(stderr,"Error in pcap_findalldevs: %s\n", errbuf);
exit(1);
}

/* 打印网络设备列表 */
for(d=alldevs; d; d=d->next)
{
printf("%d. %s", ++i, d->name);
if (d->description)
printf(" (%s)\n", d->description);
else
printf(" (No description available)\n");
}

if(i==0)
{
printf("\nNo interfaces found! Make sure WinPcap is installed.\n");
return -1;
}

/*选择网络设备接口*/
printf("Enter the interface number (1-%d):",i);
scanf("%d", &inum);

if(inum < 1 || inum > i)
{
printf("\nInterface number out of range.\n");
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}

/* 跳转到选中的适配器 */
for(d=alldevs, i=0; i< inum-1 ;d=d->next, i++);

/* 打开设备 */
if ( (adhandle= pcap_open(d->name, // 设备名
65536, // 65535保证能捕获到数据链路层上每个数据包的全部内容
PCAP_OPENFLAG_PROMISCUOUS, // 混杂模式
1000, // 读取超时时间
NULL, // 远程机器验证
errbuf // 错误缓冲池
) ) == NULL)
{
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}

/*在选中的设备接口上发送数据*/
printf("\nsending on %s...\n", d->description);

/* 发送数据包*/
//生成数据报
int packetlen=100;
unsigned char *buf= (unsigned char *)malloc(packetlen);
memset(buf,0x0,packetlen);
gen_packet(buf,packetlen); //获得生成的数据包,长度为packetlen
//开始数据包发送
if ( (ret=pcap_sendpacket(adhandle,buf,packetlen))==-1)
{
printf("发送失败\n");
free(buf);
pcap_close(adhandle);
pcap_freealldevs(alldevs);
return -1;
}

/*释放资源*/
free(buf);
pcap_close(adhandle);
pcap_freealldevs(alldevs);

return 0;
}

把代码拷贝到你的项目程序里面,可以发现现在是编译错误,有些对象根本无法识别,截图如下: 接下来就需要我们对类库进行配置啦。 首先解压你下载的开发包,随便放硬盘的某个位置,我放在了D盘的eclipse_plugins文件夹中,当然你可以随便放哪里都行。 接下来配置Eclipse,右键项目->属性->C/C++常规->项目和符号。 首先添加你的include库,在包含这个选项卡中添加你的库,点击添加->选择文件系统->选择你刚才的开发库的include文件夹,按照图中的顺序来 点击确定添加,同理在库路径选项卡中进行库路径的配置,这次添加的是lib文件夹。按照图片中的顺序来做 点击确定添加。 然后在库的选项卡中添加wpcap和Packet两个库,注意这次不能选择文件系统了,因为你指定了库路径之后它会自动搜索路径中库的名字,这次你只需要指定库的名字就好了。我之前添加的是文件系统,然后它总是提示找不到这个库,所以一定要直接填写这两个库的名字。如图所示: 添加完毕之后,出现这个样子: 好啦,点击确定,全部配置已经完毕啦。 重新构建项目,运行即可。 注意,构建过程可能出现如下问题:

  • error C2065: “PCAP_SRC_IF_STRING”: 未声明的标识符
  • error C3861: “pcap_findalldevs_ex”: 找不到标识符
  • error C2065: “PCAP_OPENFLAG_PROMISCUOUS”: 未声明的标识符
  • error C3861: “pcap_open”: 找不到标识符

因为新的版本里WinPcap支持远程数据包获取,所以还应当添加一个头文件remote-ext.h ,即#include “remote-ext.h”(记住这条语句要放在#include “pcap.h”之后,否则会出错!) 好了,一切问题都解决了,运行成功啦! 运行结果如下: 再附测试代码一例:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#define WIN32
#define HAVE_REMOTE

#include <stdio.h>
#include "pcap.h"
#include "Win32-Extensions.h"

void gen_packet(unsigned char *buf,int len);

/*生成数据包*/
void gen_packet(unsigned char *buf,int len)
{
int i=0;

//设置目标MAC地址为01:01:01:01:01:01
for (i=0;i<6;i++)
{
buf[i]=0x01;
}

//设置源MAC地址为02:02:02:02:02:02
for (i=6;i<12;i++)
{
buf[i]=0x02;
}

//设置协议标识为0xc0xd,无任何实际意义
buf[12]=0xc;
buf[13]=0xd;

//填充数据包的内容
for(i=14;i<len;i++)
{
buf[i]=i-14;
}
}


int main()
{
pcap_if_t *alldevs;
pcap_if_t *d;
int inum;
int i=0;
pcap_t *adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
int ret=-1;

/* 获取本机网络设备列表 */
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
{
fprintf(stderr,"Error in pcap_findalldevs: %s\n", errbuf);
exit(1);
}

/* 打印网络设备列表 */
for(d=alldevs; d; d=d->next)
{
printf("%d. %s", ++i, d->name);
if (d->description)
printf(" (%s)\n", d->description);
else
printf(" (No description available)\n");
}

if(i==0)
{
printf("\nNo interfaces found! Make sure WinPcap is installed.\n");
return -1;
}

/*选择网络设备接口*/
printf("Enter the interface number (1-%d):",i);
scanf("%d", &inum);

if(inum < 1 || inum > i)
{
printf("\nInterface number out of range.\n");
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}

/* 跳转到选中的适配器 */
for(d=alldevs, i=0; i< inum-1 ;d=d->next, i++);

/* 打开设备 */
if ( (adhandle= pcap_open(d->name, // 设备名
65536, // 65535保证能捕获到数据链路层上每个数据包的全部内容
PCAP_OPENFLAG_PROMISCUOUS, // 混杂模式
1000, // 读取超时时间
NULL, // 远程机器验证
errbuf // 错误缓冲池
) ) == NULL)
{
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}

/*在选中的设备接口上发送数据*/
printf("\nsending on %s...\n", d->description);

/* 发送数据包*/
//生成数据报
int packetlen=100;
unsigned char *buf= (unsigned char *)malloc(packetlen);
memset(buf,0x0,packetlen);
gen_packet(buf,packetlen); //获得生成的数据包,长度为packetlen
//开始数据包发送
if ( (ret=pcap_sendpacket(adhandle,buf,packetlen))==-1)
{
printf("发送失败\n");
free(buf);
pcap_close(adhandle);
pcap_freealldevs(alldevs);
return -1;
}

/*释放资源*/
free(buf);
pcap_close(adhandle);
pcap_freealldevs(alldevs);

return 0;
}

运行结果:

1
2
3
4
5
1. rpcap://\Device\NPF_{5AC72F8D-019C-4003-B51B-7ABB67AF392A} (Network adapter 'Microsoft' on local host)
2. rpcap://\Device\NPF_{33E23A2F-F791-409B-8452-A3FB5A78B73E} (Network adapter 'Qualcomm Atheros Ar81xx series PCI-E Ethernet Controller' on local host)
3. rpcap://\Device\NPF_{DCCF036F-A9A8-4225-B980-D3A3F0575F5B} (Network adapter 'Microsoft' on local host)
4. rpcap://\Device\NPF_{D62A0060-F424-46FC-83A5-3394081685FD} (Network adapter 'Microsoft' on local host)
5. rpcap://\Device\NPF_{B5224A53-8450-4537-AB3B-9869158121CD} (Network adapter 'Microsoft' on local host)

那么环境配好了,我们接下来就要进行计算机网络编程学习阶段了,后期的文章将会记录我计算机网络编程学习的一些内容,欢迎大家收看。

C/C++

本次为大家带来的是用Eclipse配置C/C++环境的具体步骤,希望对大家有帮助哦。 声明:如果你希望用Eclipse开发,可以参考本文章配置环境,如若想用VS开发,请看后面的文章。 本文原文地址来自 我的CSDN博客 Eclipse 配置C/C++开发环境讲解

JDK

如果没有安装JDK环境的小伙伴请自行去官网下载安装啦,安装之后的就可以跳过此步. 下载地址: JDK下载JDK环境配置

Eclipse

当然,下载JDK的目的是在Eclipse下先运行起Java程序,这样才能方便我们下一步的操作,没有Eclipse的也去下载。 下载地址: Eclipse下载 上面两步我想难度都很小,而且大部分同学已经配置了,我就不一一讲解了。

CDT

CDT是什么?它就是Eclipse的一个插件,我们需要安装了之后才可以新建C/C++项目。 注意:下载的CDT必须和Eclipse的版本对应。 下载地址: CDT下载 上面下载的Eclipse是3.7版本,在此给出适配CDT 8.0的下载链接。 下载地址: CDT 8.0下载 如果上述链接已失效,请自行百度适配你Eclipse版本的CDT进行下载。 下载之后它是一个压缩包zip格式。 不要解压直接安装即可,安装讲解如下: 打开Eclipse,进入菜单Help,选择Install New Software…,点击右边Add按钮,在Add Repository对话框中点击右下角的Archive…,浏览到你之前下载保存路径,选中cdt-master-8.0.0.zip并双击,勾选所有CDT部件,然后点击Next>,继续Next>,选中“I accept the terms of the license agreement – Finish”,点击Finish开始安装CDT。 安装完CDT后重启Eclipse。 这样我们的Eclipse的CDT插件就安装好啦,这时你会发现可以新建C/C++项目了,但是还是不能编译运行,为什么?因为没有安装编译器。接下来我们需要下载mingw编译器。

MinGW

这是一个配套Eclipse的C/C++编译器,首先我们下载下来。 官网下载地址: mingw官网下载 现在发现最新版本下载是: mingw-get-setup.exe 双击运行,选择安装路径,我的路径为:D:\mingw,安装完成之后你会发现出现一个下载库的窗口,叫Mingw Installation Manager 注意: 1. 我的类库已经安装完毕,所以每个类库前面都显示为绿色的方框,没安装之前是白色的,你需要都勾选上,然后下载安装。在每个类库上邮件单击选择make for installation即可勾选。再点击菜单中的Installation中的Update Catalogue即可安装你所勾选的类库了。 2. 安装下载可能花费不少时间,请耐心等待。 大家可以发现最左边有Basic Steup和All Packages两个分类。 第一个Basic Steup即为必须要安装的编译类库,All Packages即为所有的类库。为了保险起见,我把所有的类库都安装了。全部安装完之后即会显示绿色。 安装完之后的目录结构如下:

环境变量配置

环境变量配置: 计算机– 属性 – 高级系统设置 – 环境变量 在上方的用户变量中进行如下操作: 注意:mingw的位置不同,环境变量的配置不同,可以看最底层目录比对着来配置。我的环境变量配置如下:

(1)编辑PATH变量,在最后面加入 D:\mingw\bin D:\mingw\msys\1.0\bin D:\mingw\mingw32\bin (2)添加LIBRARY_PATH变量,内容为: D:\mingw\lib (3)添加C_INCLUDE_PATH变量,内容为: D:\mingw\include (4)添加CPLUS_INCLUDE_PATH变量,内容为: D:\mingw\lib\gcc\mingw32\4.8.1\include\c++

修改:

进入D:\mingw\bin下将mingw32-make.exe复制成make.exe。因为Eclipse使用时预设是用系统里的”make”这个文件名,而不是”mingw32-make”。

Eclipse中的配置: 窗口 -> 首选项 -> C/C++ -> New CDT Project Wizard 在右边的首选工具链的右边,工具链栏目内选择MinGW GCC,确定,如图所示: 还是在原来的 New CDT Project Wizard选项卡中,展开,Makefile项目,二进制解析器,勾选PE windows解析器,确定,如图所示。 好啦,配置工作完成,我们来测试一下新C语言项目。

测试项目

右键新建->项目->C项目 下一步,选择可执行文件,Hello World ANCI C Project 编译器选择 MinGw GCC 完成,如图所示: 完成之后的界面应该是如下所示,点击构建项目,图中的红色圆圈,构建完成之后,右键运行程序 运行结果: 欢呼吧小伙伴们 ! Eclipse 配置C/C++环境到此完全结束,如果有问题欢迎交流。

C/C++

Hi,大家好,现在计算机网络课程设计开始啦,本次为大家带来计算机网络实验的系列讲解,希望对于小伙伴们的计网课设有一定帮助哦。

写在前面

首先,博主来自山东大学,这次计网课程设计的实验学校为山东大学,年级为2012级,请对号入座。 另外,对于上学期学的计算机网络课程知识,可以说我就是为了应付考试最后狂背下来的,感觉对于计算机网络里的一些原理真的没有理解。纯粹看一些概念性的东西真的是太抽象啦,这次计网课设的目的就是让我们用编程实现网络分析技术和网络功能仿真,来深入了解计算机网络知识。可以说这些才是学习计算机网络最重要的一环,大家加油。

实验要求

1、基本任务(达标任务) 完成两台主机之间的数据通信(数据链路层)

  • 仿真ARP协议获得网段内主机的MAC表
  • 使用帧完成两台主机的通信(Hello! I’m …)

2、高端任务(优秀任务) 完成两台主机通过中间主机的数据通信(网络层)

  • 增加基于IP地址的转发功能
  • 增加网络层封装

本次任务呢分为基本任务和高端任务,当然我们的目标当然不仅限于基本任务咯。

实验概述

一句话,我们需要利用winpcap这个库用C语言编程来实现以上的任务。

winpcap讲解

那么winpcap是什么? 大多数网络应用程序通过被广泛使用的操作系统原件来访问网络,如socket。由于操作系统已经处理了底层的细节问题(如协议的处理、数据包的封装等),并提供了与读写文件类似的函数接口,因此使用该方法可以很容易的访问网络中的数据 然而有些时候,这种简单的方式并不能满足任务要求,有些应用程序需要直接访问网络中的数据包,也就是说,需要访问哪些没有被操作系统处理过的数据包 WinPcap就是为Windows平台的应用程序提供这种访问方式,提供下列功能:

  • 捕获原始数据包。无论是发送到运行WinPcap的机器上的数据包,还是在其它主机(存在网络共享介质上的主机)上进行交换的数据包,都可以被捕获
  • 在数据包传递给应用程序之前,根据用户指定的规则过滤数据包
  • 将原始数据包发送到网络上
  • 收集网络流量与网络状态的统计信息

它提供给我们什么呢?

它包含一个内核空间数据包过滤器(Netgroup Packet Filter, NPF),一个底层动态链接库(Packet.dll)和一个高层独立于操作系统的动态链接库(wpcap.dll)。各部分关系见下图

NPF:为了能够访问网络上传输的原始数据,数据包捕获系统需要绕过操作系统的协议栈。这就需要有一部分程序运行在操作系统的内核中,只有这样才能与网络接口驱动直接交互。在WinPcap中,与操作系统密切相关的是一个名为NPF的设备驱动程序,同时对不同版本的操作系统提供了不同版本的驱动程序。这些驱动程序提供了数据包捕获与发送的基本功能,同时提供了一个可编程的数据包过滤系统、一个网络监视引擎等高级功能。 动态链接库:为了让应用程序能够使用内核驱动提供的功能,数据包捕获系统必须导出相关的接口。对此,WinPcap提供两个不同层次的动态链接库:Packet.dll和wpcap.dll。 Packet.dll提供底层的API,用来直接访问驱动程序的函数,用来提供独立于微软公司不同操作系统的编程接口。 wpcap.dll库导出了更强大、更高层的捕获函数接口,具有与UNIX捕获库libpcap的兼容性。这两个库可使数据包的捕获独立于底层的网络硬件与操作系统。

实验流程

了解了winpcap之后,我们要做的就是利用它提供的类库来实现网络传输,网络解析等一系列的功能。

需要我们做好的准备有:

  • C语言的相关基础
  • 计算机网络的基础知识
  • 对开发环境的相关了解

如果对于上述中任何一个不熟悉,请先去补习一下基础知识吧。

参考书目

  • 吴功宜等,《计算机网络高级软件编程技术》(第2版),清华大学出版社
  • 徐恪等, 《高级计算机网络》,清华大学出版社
  • A.S. Tannenbaum,《计算机网络》(第5版),清华大学出版社
  • 吕雪峰等, 《网络分析技术揭秘(原理、实践与WinPcap深入解析》,机械工业出版社

结语

下一节,我们将讲解在Eclipse环境下搭建winpcap环境的具体步骤。本次讲解到此结束,谢谢大家。

个人日记

二零一五年七月九日,伴随着最后一门图形学交卷铃声的想起,我的大三正式结束了。首先很抱歉,最近一段时间一直在忙考试复习,时间也比较紧张,另外就是整理各种材料,还有准备一些面试,也算是忙得不亦乐乎,所以,这段时间也没有去编代码,大约也有一个月的时间没有更新博客了,有好多热心的朋友在我博客留言评论,也没有及时回复,在这里向大家说一声抱歉。放假回家之后,我会好好研究一下大家提出的意见和建议,给予大家回复的。 恩,接下来的日子应该会稍微轻松那么一点,自己的研究生路大体有了轮廓,有了规划。将来想从事的方向为信息安全方向,自己也做过一些项目和网站,对一些安全性问题要求也是比较高的,自己也对安全问题比较感兴趣,嗯。 现在时间是二零一五年七月十二日,昨天回家,收拾整顿一番,今天假期正式开始啦! 上学期是我大学的第六个学期了,对于保研来说,也着实是最后的一个重要的学期了,学习上也要更加重视。仔细回想起来,这个学期实际的项目开发没有做过多少,和同学开发了一个演唱会支付系统,又完善了一下自己之前的项目。学期后期,基本上就是准备一些面试和靠前复习,另外还有一些研究生招生的面试咨询等等,结果就不透露啦,自我感觉还是不错。 本来有好多话想说,然而这个时间,却不想多言了。 心里的石头还没落地,最近的日子,好好准备即将到来的面试。暑假到了,想要学习的东西还有很多,重新开启我的代码之路!Come On!

崔庆才

2015/7/12

C/C++

综述

通过上一节说的绘制 3D 图形基础,我们应该对绘制 3D 图形有了基本的认识,接下来我们就进行一个实例,绘制一个 3D 机器人。 本节我们要完成的任务有:

1.绘制一个仿真 3D 机器人(样式自选,参考例图),至少包含头、躯干、四肢三个部分. 2.对机器人填充颜色。 3.增加点光源,使得机器人更加真实。 4.实现交互,使得能够控制机器人进行旋转、前进、后退等动作。 键盘”w”:前进 键盘”s”:后退 键盘”a”:顺时针旋转 键盘”d”:逆时针旋转

接下来我们就一步步实现机器人的绘制吧。

绘制球体

绘制球体我们有两种方法,一个叫 glutSolidSphere,另一个叫 glutWireSphere,这两个的区别在于,一个绘制的是实心的球体,另一个绘制的是现状描绘而成的球体,在绘制过程中,我们可以通过一个参数来对其进行控制。 glutSolidSphere 是 GLUT 工具包中的一个函数,该函数用于渲染一个球体。球体球心位于原点。在 OpenGL 中默认的原点就是窗口客户区的中心。 函数原型

1
2
void glutSolidSphere(GLdouble radius , GLint slices , GLint stacks);
void glutWireSphere(GLdouble radius, GLint slices, GLint stacks );

radius,球体的半径 slices,以 Z 轴上线段为直径分布的圆周线的条数(将 Z 轴看成地球的地轴,类似于经线) stacks,围绕在 Z 轴周围的线的条数(类似于地球上纬线) 一般而言, 后两个参数赋予较大的值, 渲染花费的时间要长, 效果更逼真。 然而我们可以发现,这里并没有定义球中心的参数,所以,我们可以利用平移函数组合实现。即利用 glTranslated 函数来实现。最后,我们定义的画球体的方法如下

1
2
3
4
5
6
7
8
9
10
11
//画球
void drawBall(double R, double x, double y,double z, int MODE) {
glPushMatrix();
glTranslated(x,y,z);
if (MODE == SOLID) {
glutSolidSphere(R,20,20);
} else if (MODE ==WIRE) {
glutWireSphere(R,20,20);
}
glPopMatrix();
}

其中两个常量定义如下

1
2
#define SOLID 1
#define WIRE 2

在这里我们还用到了上一节所说的 PushMatrix 和 PopMatrix 方法。如果不熟悉,请查看上一节的内容。

绘制长方体

同样地,利用变换平移放缩的方法,再加上类库的绘制正方体的方法,我们也可以轻松地实现绘制长方体的方法。 正方体怎样变成长方体,很简单,拉伸一下就好了。所以,我们用到了 glScaled 方法。 绘制长方体的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
//画长方体
void drawSkewed(double l, double w, double h, double x, double y, double z, int MODE) {
glPushMatrix();
glScaled(l, w, h);
glTranslated(x, y, z);
if (MODE == SOLID) {
glutSolidCube(1);
} else if (MODE == WIRE) {
glutWireCube(1);
}
glPopMatrix();
}

这里仍然还是定义了绘图模式,是线条还是实体。

定义光照

在这里提供一篇博文,讲光照讲得比较细致 OPENGL 光照 那么在这里我就直接贴上光照设置的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void init() {
//定义光源的颜色和位置
GLfloat ambient[] = { 0.5, 0.8, 0.1, 0.1 };
GLfloat diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat position[] = { -80.0, 50.0, 25.0, 1.0 };
//选择光照模型
GLfloat lmodel_ambient[] = { 0.4, 0.4, 0.4, 1.0 };
GLfloat local_view[] = { 0.0 };
glClearColor(0.0, 0.0, 0.0, 0.0);
glShadeModel(GL_SMOOTH);
//设置环境光
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
//设置漫射光
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
//设置光源位置
glLightfv(GL_LIGHT0, GL_POSITION, position);
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
glLightModelfv(GL_LIGHT_MODEL_LOCAL_VIEWER, local_view);
//启动光照
glEnable(GL_LIGHTING);
//启用光源
glEnable(GL_LIGHT0);
}

其中设置了光源颜色,位置等参数。

图形的移动

在这里我们实现了鼠标的监听旋转和键盘的监听旋转,在这里分别描述如下

1.鼠标监听

对于鼠标监听事件,在前面的文章中已经做了说明,如果大家不熟悉可以看下面这篇文章 鼠标监听 我们要实现的就是在拖动鼠标的时候实现图形的旋转功能。 在点击鼠标时,我们记录下来点击的位置,然后在鼠标移动的时候记录下当前坐标与上一个位置的坐标之差。通过定义一个角度的变量,每次鼠标移动的时候让整个图形旋转角度加上这个差值,然后重新绘制图形,就可以实现整个图形的 旋转了。 鼠标监听的两个方法如下,分别是鼠标点击和鼠标移动。 定义两个变量,旋转角度

1
2
int spinX = 0;
int spinY = 0;

然后定义两个鼠标事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 鼠标移动事件
void mouseMove(int x, int y) {
int dx = x - moveX;
int dy = y - moveY;
printf("dx;%dx,dy:%dy\n",dx,dy);
spinX += dx;
spinY += dy;
glutPostRedisplay();
moveX = x;
moveY = y;
}
//鼠标点击事件
void mouseClick(int btn, int state, int x, int y) {
moveX = x;
moveY = y;
}

恩,通过定义上面的方法,然后在 main 中加入监听。

1
2
3
4
//鼠标点击事件,鼠标点击或者松开时调用
glutMouseFunc(mouseClick);
//鼠标移动事件,鼠标按下并移动时调用
glutMotionFunc(mouseMove);

display 函数中,在绘图前调用该方法即可

1
2
glRotated(spinX, 0, 1, 0);
glRotated(spinY, 1, 0, 0);

分别是绕 y 轴和 x 轴旋转一定的角度。这样就可以实现鼠标的监听了。

2.键盘监听

键盘事件也很简单,同样是监听按键的按下。其中 w、s 键是用来控制机器人的远近的。 定义一个变量叫 dis,代表远近 键盘事件函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//键盘事件
void keyPressed(unsigned char key, int x, int y) {
switch (key) {
case 'a':
spinX -= 2;
break;
case 'd':
spinX += 2;
break;
case 'w':
des += 2;
break;
case 's':
des -= 2;
break;
}
glutPostRedisplay();
}

在 main 函数中加入监听

1
2
//键盘事件
glutKeyboardFunc(keyPressed);

display 函数中加入如下的变换

1
2
3
glRotated(spinX, 0, 1, 0);
glRotated(spinY, 1, 0, 0);
glTranslated(0, 0, des);

这样通过上述方法,我们便可以实现鼠标和键盘的监听了。

裁切物体

在这里我们可能要画一个半圆,那么最方便的方法就是裁切了,利用下面的函数,我们可以方便地实现。

void glClipPlane(GLenum plane, const GLdouble *equation);

定义一个裁剪平面。equation 参数指向平面方程 Ax + By + Cz + D = 0 的 4 个系数。 equation=(0,-1,0,0),前三个参数(0,-1,0)可以理解为法线向下,只有向下的,即 Y<0 的才能显示,最后一个参数 0 表示从 z=0 平面开始。这样就是裁剪掉上半平面。 equation=(0,1,0,0)表示裁剪掉下半平面, equation=(1,0,0,0)表示裁剪掉左半平面, equation=(-1,0,0,0)表示裁剪掉右半平面, equation=(0,0,-1,0)表示裁剪掉前半平面, equation=(0,0,1,0)表示裁剪掉后半平面 代码示例如下

1
2
3
4
5
GLdouble eqn[4]={0.0,0.0,-1.0,0.0};
glClipPlane(GL_CLIP_PLANE0,eqn);
glEnable(GL_CLIP_PLANE0);
glutSolidSphere(headR,slices,slices);
glDisable(GL_CLIP_PLANE0);

首先我们必须要定义一个 GLdouble 数组,然后利用 glClipPlane 方法来设置,然后开启裁切,最后关闭裁切。 利用类似的方法我们可以写出绘制半球的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//画半球
void drawHalfBall(double R, double x, double y,double z, int MODE) {
glPushMatrix();
glTranslated(x,y,z);
GLdouble eqn[4]={0.0, 1.0, 0.0, 0.0};
glClipPlane(GL_CLIP_PLANE0,eqn);
glEnable(GL_CLIP_PLANE0);
if (MODE == SOLID) {
glutSolidSphere(R,20,20);
} else if (MODE ==WIRE) {
glutWireSphere(R,20,20);
}
glDisable(GL_CLIP_PLANE0);
glPopMatrix();
}

恩,通过上述方法,我们可以方便地绘制出一个半球体。

绘制机器人

有了上述的铺垫,我们绘制机器人简直易如反掌,同样还可以实现各式各样的监听。 主要就是位置的确定了。 display 函数如下

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
void display(void) {
//清除缓冲区颜色
glClear(GL_COLOR_BUFFER_BIT);
//定义白色
glColor3f(1.0, 1.0, 1.0);
//圆点放坐标中心
glLoadIdentity();
//从哪个地方看
gluLookAt(-2.0, -1.0, 20.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
glPushMatrix();
glRotated(spinX, 0, 1, 0);
glRotated(spinY, 1, 0, 0);
glTranslated(0, 0, des);
//头
drawBall(2, 0, 1, 0, SOLID);
//身体
drawSkewed(5, 4.4, 4, 0, -0.75, 0, SOLID);
//肩膀
drawHalfBall(1, 3.5, -2.1, 0, SOLID);
drawHalfBall(1, -3.5, -2.1, 0, SOLID);
//胳膊
drawSkewed(1, 3, 1, 3.5, -1.3, 0, SOLID);
drawSkewed(1, 3, 1, -3.5, -1.3, 0, SOLID);
//手
drawBall(1, 3.5, -6.4, 0, SOLID);
drawBall(1, -3.5, -6.4, 0, SOLID);
//腿
drawSkewed(1.2, 3, 2, 1, -2.4, 0, SOLID);
drawSkewed(1.2, 3, 2, -1, -2.4, 0, SOLID);
//脚
drawSkewed(1.5, 1, 3, 0.9, -9.2, 0, SOLID);
drawSkewed(1.5, 1, 3, -0.9, -9.2, 0, SOLID);
glPopMatrix();
glutSwapBuffers();
}

恩,通过调用这个函数我们便可以完成机器人的绘制了。

完整代码

在这里提供完整代码示例,仅供参考

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#include <glut.h>
#include <stdlib.h>
#include <stdio.h>

#define SOLID 1
#define WIRE 2

int moveX,moveY;
int spinX = 0;
int spinY = 0;
int des = 0;


void init() {
//定义光源的颜色和位置
GLfloat ambient[] = { 0.5, 0.8, 0.1, 0.1 };
GLfloat diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat position[] = { -80.0, 50.0, 25.0, 1.0 };
//选择光照模型
GLfloat lmodel_ambient[] = { 0.4, 0.4, 0.4, 1.0 };
GLfloat local_view[] = { 0.0 };
glClearColor(0.0, 0.0, 0.0, 0.0);
glShadeModel(GL_SMOOTH);
//设置环境光
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
//设置漫射光
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
//设置光源位置
glLightfv(GL_LIGHT0, GL_POSITION, position);
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
glLightModelfv(GL_LIGHT_MODEL_LOCAL_VIEWER, local_view);
//启动光照
glEnable(GL_LIGHTING);
//启用光源
glEnable(GL_LIGHT0);
}

//画球
void drawBall(double R, double x, double y,double z, int MODE) {
glPushMatrix();
glTranslated(x,y,z);
if (MODE == SOLID) {
glutSolidSphere(R,20,20);
} else if (MODE ==WIRE) {
glutWireSphere(R,20,20);
}
glPopMatrix();
}

//画半球
void drawHalfBall(double R, double x, double y,double z, int MODE) {
glPushMatrix();
glTranslated(x,y,z);
GLdouble eqn[4]={0.0, 1.0, 0.0, 0.0};
glClipPlane(GL_CLIP_PLANE0,eqn);
glEnable(GL_CLIP_PLANE0);
if (MODE == SOLID) {
glutSolidSphere(R,20,20);
} else if (MODE ==WIRE) {
glutWireSphere(R,20,20);
}
glDisable(GL_CLIP_PLANE0);
glPopMatrix();
}

//画长方体
void drawSkewed(double l, double w, double h, double x, double y, double z, int MODE) {
glPushMatrix();
glScaled(l, w, h);
glTranslated(x, y, z);
if (MODE == SOLID) {
glutSolidCube(1);
} else if (MODE ==WIRE) {
glutWireCube(1);
}
glPopMatrix();
}

void display(void) {
//清除缓冲区颜色
glClear(GL_COLOR_BUFFER_BIT);
//定义白色
glColor3f(1.0, 1.0, 1.0);
//圆点放坐标中心
glLoadIdentity();
//从哪个地方看
gluLookAt(-2.0, -1.0, 20.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
glPushMatrix();
glRotated(spinX, 0, 1, 0);
glRotated(spinY, 1, 0, 0);
glTranslated(0, 0, des);
//头
drawBall(2, 0, 1, 0, SOLID);
//身体
drawSkewed(5, 4.4, 4, 0, -0.75, 0, SOLID);
//肩膀
drawHalfBall(1, 3.5, -2.1, 0, SOLID);
drawHalfBall(1, -3.5, -2.1, 0, SOLID);
//胳膊
drawSkewed(1, 3, 1, 3.5, -1.3, 0, SOLID);
drawSkewed(1, 3, 1, -3.5, -1.3, 0, SOLID);
//手
drawBall(1, 3.5, -6.4, 0, SOLID);
drawBall(1, -3.5, -6.4, 0, SOLID);
//腿
drawSkewed(1.2, 3, 2, 1, -2.4, 0, SOLID);
drawSkewed(1.2, 3, 2, -1, -2.4, 0, SOLID);
//脚
drawSkewed(1.5, 1, 3, 0.9, -9.2, 0, SOLID);
drawSkewed(1.5, 1, 3, -0.9, -9.2, 0, SOLID);
glPopMatrix();
glutSwapBuffers();
}
//鼠标点击事件
void mouseClick(int btn, int state, int x, int y) {
moveX = x;
moveY = y;
GLfloat ambient[] = { (float)rand() / RAND_MAX, (float)rand() / RAND_MAX, (float)rand() / RAND_MAX, 0.1 };
//设置环境光
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
//启用光源
glEnable(GL_LIGHT0);
}

//键盘事件
void keyPressed(unsigned char key, int x, int y) {
switch (key) {
case 'a':
spinX -= 2;
break;
case 'd':
spinX += 2;
break;
case 'w':
des += 2;
break;
case 's':
des -= 2;
break;
}
glutPostRedisplay();
}
// 鼠标移动事件
void mouseMove(int x, int y) {
int dx = x - moveX;
int dy = y - moveY;
printf("dx;%dx,dy:%dy\n",dx,dy);
spinX += dx;
spinY += dy;
glutPostRedisplay();
moveX = x;
moveY = y;
}

void reshape(int w, int h) {
//定义视口大小
glViewport(0, 0, (GLsizei) w, (GLsizei) h);
//投影显示
glMatrixMode(GL_PROJECTION);
//坐标原点在屏幕中心
glLoadIdentity();
//操作模型视景
gluPerspective(60.0, (GLfloat) w/(GLfloat) h, 1.0, 20.0);
glMatrixMode(GL_MODELVIEW);
}

int main(int argc, char** argv) {
//初始化
glutInit(&argc, argv);
//设置显示模式
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
//初始化窗口大小
glutInitWindowSize(500, 500);
//定义左上角窗口位置
glutInitWindowPosition(100, 100);
//创建窗口
glutCreateWindow(argv[0]);
//初始化
init();
//显示函数
glutDisplayFunc(display);
//窗口大小改变时的响应
glutReshapeFunc(reshape);
//鼠标点击事件,鼠标点击或者松开时调用
glutMouseFunc(mouseClick);
//鼠标移动事件,鼠标按下并移动时调用
glutMotionFunc(mouseMove);
//键盘事件
glutKeyboardFunc(keyPressed);
//循环
glutMainLoop();
return 0;
}

仅供参考,如有问题,敬请指正。 运行结果如下 QQ截图20150607013139 恩,大体就是这样。

总结

本次实验做的比较匆忙,只研究了一个晚上的时间,所以有些地方还是不太完善,希望发出来对小伙伴们有所启发,有所帮助。如果有问题,欢迎同我交流。谢大家!

C/C++

综述

在前面我们进行了 2D 图形的绘制,接下来,我们将步入 3D 图形的世界,绘制出一个 3D 机器人,好,废话不多说,让我们一起来迈入 3D 绘图之旅吧。

基本函数

那么在绘图之前呢,我们首先要介绍几个新的函数,弄懂了这几个函数我们才能方便地绘制出我们的 3D 机器人。 那么我们介绍一下gluPerspectivegluLookAt、glPushMatrix、glPopMatrix()函数

1.void gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar)

参数说明: fovy,视角的大小,如果设置为 0,相当闭上眼睛,什么也看不到,如果为 180,则可以看到几乎所有的内容。一般设置为 60 即可。 aspect,就是实际窗口的纵横比,即 x/y,一般都是要按照窗口比例来,否则看到的图形和实际图形就会不一样了。 zNear,表示近处的裁面。 zFar,表示远处的裁面。 gluPerspective 这个函数指定了观察的视景体在世界坐标系中的具体大小,一般而言,其中的参数 aspect 应该与窗口的宽高比大小相同。比如说,aspect=2.0 表示在观察者的角度中物体的宽度是高度的两倍,在视口中宽度也是高度的两倍,这样显示出的物体才不会被扭曲。 详细说明 03087bf40ad162d9f337731d11dfa9ec8b13cdd1

2.void gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,GLdouble centerx,GLdouble centery,GLdouble centerz,GLdouble upx,GLdouble upy,GLdouble upz)

该函数定义一个视图矩阵,并与当前矩阵相乘。

第一组 eyex, eyey,eyez 相机在世界坐标的位置

第二组 centerx,centery,centerz 相机镜头对准的物体在世界坐标的位置

第三组 upx,upy,upz 相机向上的方向在世界坐标中的方向

你把相机想象成为你自己的脑袋:

第一组数据就是脑袋的位置

第二组数据就是眼睛看的物体的位置

第三组就是头顶朝向的方向(因为你可以歪着头看同一个物体)

在这里,我们就涉及到一个十分重要的概念,坐标系。 在 3D 绘图中,坐标系是右手系,如果 x 轴指向我们的右侧,那么 y 则指向上侧,z 轴指向屏幕外。如下图所示。 20150606234046 理解了上述两个函数,下面我们来用一个小例子感受一下 运行如下代码

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
#include <glut.h>
#include <stdlib.h>


void display(void) {
//清除缓冲区颜色
glClear(GL_COLOR_BUFFER_BIT);
//定义白色
glColor3f(1.0, 1.0, 1.0);
//圆点放坐标中心
glLoadIdentity();
//从哪个地方看
gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
glutWireTeapot(2);
//清除缓冲区刷新
glutSwapBuffers();
}

void reshape(int w, int h) {
//定义视口大小
glViewport(0, 0, (GLsizei) w, (GLsizei) h);
//投影显示
glMatrixMode(GL_PROJECTION);
//坐标原点在屏幕中心
glLoadIdentity();
//操作模型视景
gluPerspective(60.0, (GLfloat) w/(GLfloat) h, 1.0, 20.0);
glMatrixMode(GL_MODELVIEW);
}

int main(int argc, char** argv) {
//初始化
glutInit(&argc, argv);
//设置显示模式
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
//初始化窗口大小
glutInitWindowSize(500, 500);
//定义左上角窗口位置
glutInitWindowPosition(100, 100);
//创建窗口
glutCreateWindow(argv[0]);
//清除颜色缓冲区
glClearColor(0.0, 0.0, 0.0, 0.0);
//显示函数
glutDisplayFunc(display);
//窗口大小改变时的响应
glutReshapeFunc(reshape);
//循环
glutMainLoop();
return 0;
}

运行结果如下 QQ截图20150606234448 在这里,我们定义了

1
gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

意思就是把我们的眼睛放到了(0,0,5)的位置,茶壶放到了原点,脑袋顶朝上,即正视茶壶。 如果我们改变了几个参数的值,例如

1
gluLookAt(0.0, 0.0, 5.0, 4.0, 1.0, 0.0, 0.0, 0.0, 1.0);

眼睛位置不变,茶壶位置变为(4,1,0),脑袋顶的方向朝屏幕外侧,那么看到的效果如下。 QQ截图20150606234708 大家可以通过更改这些参数的值来对视图进行变换。

3.glPushMatrix()、glPopMatrix()

这个方法在绘制 3D 图形时依然十分有用,因为我们需要绘制好多个图形,绘制多个不一样的图形必然要经过各种矩阵变换,所以,少不了的利用平移变换放缩的功能,所以为了保证每个图形不受影响,在绘制一个新图形前我们只要调用一下 glPushMatrix 函数,保存当前的变换矩阵,然后绘制完一个图形之后再调用 glPopMatrix 函数来恢复之前的变换矩阵就可以了。 比如,绘制球体

1
2
3
4
glPushMatrix();
glTranslated(x,y,z);
glutWireSphere(R,20,20);
glPopMatrix();

在两个函数中间我们可以进行任意的变换放缩等操作,然后绘制想要的图形即可。

绘图函数

接下来我们说一下一些常用的绘图函数,直接用类库函数来绘制即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void glutWireSphere(GLdouble radius, GLint slices, GLint stacks); 线框球
void glutSolidSphere(GLdouble radius, GLint slices, GLint stacks); 实心球
void glutWireCube(GLdouble size); 线框立方体
void glutSolidCube(GLdouble size); 实心立方体
void glutWireTorus(GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings); 线框圆环
void glutSolidTorus(GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings); 实心圆环
void glutWireIcosahedron(void); 线框20面体
void glutSolidIcosahedron(void); 实心20面体
void glutWireOctahedron(void); 线框8面体
void glutSolidOctahedron(void); 实心8面体
void glutWireTetrahedron(void); 线框4面体
void glutSolidTetrahedron(void); 实心4面体
void glutWireDodecahedron(GLdouble radius); 线框12面体
void glutSolidDodecahedron(GLdouble radius); 实心12面体
void glutWireCone(GLdouble radius, GLdouble height, GLint slices, GLint stacks); 线框圆锥体
void glutSolidCone(GLdouble radius, GLdouble height, GLint slices, GLint stacks); 实心圆锥体
void glutWireTeapot(GLdouble size); 线框茶壶
void glutSolidTeapot(GLdouble size); 实心茶壶

利用上面的绘图函数我们便可以方便地实现各种图形的绘制。

结语

那么本节就先告一段落,本节介绍了相关的绘图函数以及几个重要的函数,为接下来我们绘制机器人做下铺垫,希望小伙伴们可以好好理解。

PHP

综述

有时我们在textarea中输入了正常的文本内容,然而如果直接将文本取出来呈现在网页中,原本的换行和回车功能便会失效,完全丢失了原来的样式,这是因为在HTML中的空格和换行分别为 和
,所以我们需要将其替换才能变成我们想要的样式。 注:本篇内容非常基础,仅作参考,不喜勿喷。

替换函数

我们可以定义这样的一个函数,将原来的字符串替换为HTML文本。即把空格和换行替换掉即可

1
2
3
4
5
6
function htmtocode($content) { 
$content = htmlspecialchars($content, ENT_QUOTES);
$content = str_replace(" ", "&nbsp;", $content);
$content = str_replace("\n", "<br>",$content);
return $content;
}

通过上述方法,我们首先将HTML的特殊标记转化,然后替换掉空格和换行即可。

用法

写个最简单的小例子,在textarea中输入任意的文本,存储到数据库,然后再取出来,可以实现textarea输入的效果,完全一样。 数据库名称:demo 数据库表:

1
2
3
4
5
CREATE TABLE IF NOT EXISTS `article` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`text` text NOT NULL,
PRIMARY KEY (`id`)
)

插入数据:

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
<head>
<meta charset="utf-8">
</head>
<body>
<form action="" method="post">
<textarea name="text"></textarea>
<input type="submit" name="sub">
</form>
</body>
<?php
$con = mysql_connect("localhost", "root", "");
if (!$con) {
die("数据库连接失败");
}
mysql_select_db("demo",$con);
mysql_query("set names utf8");
$text = @$_POST['text'];
$sql = "insert into article(text) values ('$text')";
echo $sql;
$result = mysql_query($sql,$con);
if ($result) {
echo "录入成功";
} else {
echo "录入失败";
}

?>

查询数据:

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
<head>
<meta charset="utf-8">
</head>
<body>
<form action="" method="post">
<textarea name="text"></textarea>
<input type="submit" name="sub">
</form>
</body>
<?php
$con = mysql_connect("localhost", "root", "");
if (!$con) {
die("数据库连接失败");
}
mysql_select_db("demo",$con);
mysql_query("set names utf8");
$text = @$_POST['text'];
$sql = "select * from article";

$result = mysql_query($sql,$con);
if ($result) {
while ($row = mysql_fetch_array($result)) {
echo htmtocode($row['text']);
}
} else {
echo "录入失败";
}



function htmtocode($content) {
$content = htmlspecialchars($content, ENT_QUOTES);
$content = str_replace(" ", "&nbsp;", $content);
$content = str_replace("\n", "<br>",$content);
return $content;
}
?>

嗯,通过上面的方法我们便可以正常地实现输入内容和输出内容的匹配呈现。

JavaScript

前言

今天发现博客评论的默认头像不显示了,博客使用的是多说评论,不知道啥情况,之前设置过一个默认头像,然而今天图片都加载不出来了,可对于我这种强迫症来说,这能忍吗?你能忍我也不能忍啊,这必须不能忍啊,看到一个个的红叉号简直刺透了心。于是,我决定,必须要干掉它!

初步尝试

默认头像不行了,我就重新上传个默认头像对吧,简单粗暴易行。来来来,说试就试! 20150525114559 然而上传完毕之后,它是这个样子滴,依然不给我面子! 20150525114655 你在逗我吗?是不是多说服务器哪里出毛病了?

究其原因

然而,美好的事情总是意想不到的发生,当我知道真相的一刻,整个人都惊呆了。 浏览器里审查一下元素,看它到底引用了神马鬼。 20150525115038 好,我就打开这个链接,惊呆了。简直棒到无极限! 域名已过期!域名已过期!域名已过期!重要的话说三遍!简直溜到不行! 20150525115447 好吧,这真是太棒了!!我还能说些什么!!!

峰回路转

行,既然这样,那我自己提供 CDN,用我自己的七牛云存储! 那就用 jQuery 好了,方便简单,在</head>标签前面加上这么个代码,来试试。

1
2
3
4
5
$(document).ready(function(){
$('.ds-avatar img[src*="cdncache"]').each(function(){
$(this).attr("src",'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112155.jpg');
})
});

意思是,在页面加载完成之后,把 src 里面包含 cdncache 的多说头像 img 替换掉。 这句话最好加在所有 JS 的最后面,要不然可能不会生效。JS 执行是有先后顺序的。 这样,我们就会发现,头像已经奇迹般地发生了变化。 20150525123151 本身有头像的仍然是那个头像,使用默认头像的已经替换了新头像。

更上一层

然而这并不能满足我的愿望啊,我想要多变的头像样式,就是这么任性! 来来来,改代码,我再传上几个头像,让默认头像的孩纸们随机地使用我的头像,一定是炫爆了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script type="text/javascript">
$(document).ready(function(){
var imgs = new Array();
imgs[0] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525111154.jpg';
imgs[1] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525111447.jpg';
imgs[2] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112058.jpg';
imgs[3] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112112.jpg';
imgs[4] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112129.jpg';
imgs[5] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112155.jpg';
$('.ds-avatar img[src*="cdncache"]').each(function(){
var rand = Math.floor(Math.random()*imgs.length);
$(this).attr("src",imgs[rand]);
})
});
</script>

嗯,把刚才代码优化成这样,我们就可以发现,整个网站的头像就被替换成我们想要的效果了! 20150525123537 简直酷炫!小伙伴们可以尝试一下!

C/C++

综述

在上一篇文章中我们已经实现了图形的取点绘制,接下来我们还要实现的功能有图形的删除、移动以及存盘读盘功能。 概述如下

  • 鼠标点击某个绘制的图形,将其删除
  • 鼠标拖动某个图形,可以对其随意拖拽改变位置
  • 点击存盘,将图形的位置及颜色保存到文件
  • 点击读盘,将图形的位置颜色从文件中读取出来并重新绘制

好,接下来我们在上一篇的基础上进行一步步的说明。

图形删除

首先像之前一样,我们定义一个删除的按钮,指定好颜色。

1
GLubyte delBtn[3] = {30, 30, 30};

在 display 函数中绘制一个删除的按钮

1
glColorCircle(220, 280, 10, delBtn);

顶部变变成了这个样子,其中最左边的那个按钮便是删除按钮 QQ截图20150511124225 接下来我们想一下删除的实现。 首先我们点击删除按钮之后要设置一下 drawStatus,比如设置为 2 代表删除。 然后鼠标点击下方任意位置时,如果当前 drawStatus 为 2,如果鼠标处在一个多边形内部,我们就把它删除掉。 那么就会出现两个问题了,怎样判断点击的点处在多边形的内部?怎样执行删除操作? 下面我们分别来研究一下

1. 判断点在多边形内部

思路:求解通过该点的水平线与多边形各边的交点 ,如果交点为奇数,那么就判定为在多边形内部,否则在外部。 因此,我的算法实现如下,传入的参数一个是 ploygon 结构体,结构体中包含了顶点坐标,另外就是这个点的 x,y 坐标。 思路参考来源: 判断点在多边形内的多种写法 经过我的改写,变成了如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 判断一个点是否在多边形内部 */
bool pointInPoly(polygon poly, int x, int y){
int nCross = 0;
for (int i = 0; i < poly.verNum; i++) {
int p1x = poly.x[i];
int p1y = poly.y[i];
int p2x = poly.x[(i + 1) % poly.verNum];
int p2y = poly.y[(i + 1) % poly.verNum];
/* 平行或在延长线上,忽略 */
if (p1y == p2y || y < min(p1y, p2y) || y > max(p1y, p2y)) {
continue;
}
/* 计算交点横坐标 */
double m = (double)(y - p1y) * (double)(p2x - p1x) / (double)(p2y - p1y) + p1x;
if ( m > x ) nCross++;
}
return (nCross % 2 == 1);
}

以上便是判断在多边形内部的算法。

2.删除图形实现

好了,有了上面的算法,我们就依次查找我们声明的 polygons 数组,找到第一个符合要求的多边形结构体 polygon 返回它的索引。 这个方法就是传入了点击点的位置 x,y 坐标,然后判断是在哪个多边形内,然后移除之。如果没有找到,则不执行删除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 删除多边形 */
void deletePoly(int x, int y) {
int delenum = -1;
for (int i = 0; i< con; i++) {
if (pointInPoly(polygons[i], x, y)) {
delenum = i;
}
}
if (delenum != -1) {
removePoly(delenum);
}
/* 重新绘制 */
glutPostRedisplay();
}

其中 removePoly(delenum) 就是通过传入这个结构体的索引,然后将它从 polygons 里面删除即可,方法实现如下

1
2
3
4
5
6
7
8
9
10
11
/* 删除多边形 */
void removePoly(int index)
{
if (index>con) {
printf("Out Of Index\n");
}
for (int i = index; i < con; i++) {
polygons[i] = polygons[i+1];
}
con--;
}

其实就是相当于从数组中移除一个元素,然后后面的元素依次前移。 恩好了,准备工作都做好了,我们只需要在点击了删除按钮之后,调用一下 deletePoly 函数即可。 把原来的 mouseClick 方法改写如下即可,代码较长,不完全展开,双击查看完整代码

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
54
55
56
57
58
59
60
61
62
63
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
} else if (sameColor(color, delBtn)) {
/* 删除时设置drawStatus为2 */
drawStatus = 2;
printf("drawStatus:%d\n", drawStatus);
}

/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}else if (drawStatus == 2) {
/* 删除图形 */
deletePoly(x, y);
}
}
}
}

经过上面的步骤,我们便可以很方便地实现图形的删除了。运行结果如下

[embed]http://cdn.cuiqingcai.com/robot2.mp4[/embed]

嗯,就是这样,现在删除就大功告成了。

移动图形

像之前一样,定义一个移动图形的按钮。在此不再赘述

1
GLubyte moveBtn[3] = {40, 40, 40};
1
glColorCircle(190, 280, 10, moveBtn);

那么图形移动怎么实现?思路如下 首先利用判断点击的点是否在多边形内部的算法,找到需要移动的多边形。然后在监听鼠标移动的事件中设置这个多边形各个顶点的相对移动距离等于鼠标相对移动距离即可。 首先,移动的 drawSatus 我们设置为 3,在 mouseClick 方法中加入如下部分,在哪加你应该已经知道了。

1
2
3
4
5
else if (sameColor(color, moveBtn)) {
/* 移动时设置drawStatus为3 */
drawStatus = 3;
printf("drawStatus:%d\n", drawStatus);
}

在 mouseMove 方法中(另一个监听鼠标移动的事件)中首先判断当前的 drawStatus 如果为 3,那么就执行移动操作。 加入如下的方法即可。

1
2
3
4
5
6
7
8
/* 鼠标移动事件 */
void mouseMove(int x, int y)
{
if(drawStatus == 3 && moveNum != -1){
printf("move x:%d,y:%dmoveNum:%d\n", x, y,moveNum);
movePoly(x, y);
}
}

这里两点需要说明,moveNum 就是我们需要移动的图形的索引,定义为了全局变量。在鼠标点击时,首先判定点击点在哪个图形中,然而因为有两个鼠标事件(一个是点击,一个是移动),所以就把这个变量定义为全局变量,来标记该移动哪个图形。moveNum 的初始值为-1,如果为-1,那么就代表没有选中任何图形,不移动。如果不是-1,就进行移动。 其次,movePloy 方法实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 移动多边形 */
void movePoly(int x, int y) {
/* 取到要移动的图形 */
int verNum = polygons[moveNum].verNum;
printf("verNum%d,x%d,y%d,clickX%d,clickY%d\n",verNum,x,y,clickX,clickY);
for (int i = 0; i < verNum; i++) {
polygons[moveNum].x[i] += (x - clickX);
polygons[moveNum].y[i] += (y - clickY);
printf("polyx%d\n",polygons[moveNum].x[i]);
}
/* 移动完毕之后重新记录当前的点 */
clickX = x;
clickY = y;
/* 重新绘制 */
glutPostRedisplay();
}

这个方法的实现有一部分需要说明,首先是 clickX,clickY。 因为在移动时移动的是相对距离。所以,鼠标每移动一个像素,我们就要计算下一步鼠标移动的位置和上一个位置的差值,所以每次移动我们都需要标记一下点击的位置。所以上面的方法中才有了

1
2
3
/* 移动完毕之后重新记录当前的点 */
clickX = x;
clickY = y;

然后,在图形移动时,我们需要移动的是相对位置。所以我们将一个图形的所有顶点进行遍历,加上鼠标移动的相对位置即可。就是如下的实现

1
2
3
4
for (int i = 0; i < verNum; i++) {
polygons[moveNum].x[i] += (x - clickX);
polygons[moveNum].y[i] += (y - clickY);
}

另外,mouseClick 方法中我们需要调用一下即可。mouseClick 方法现在改写如下

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 赋值全局变量 */
clickX = x;
clickY = y;
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
} else if (sameColor(color, delBtn)) {
/* 删除时设置drawStatus为2 */
drawStatus = 2;
printf("drawStatus:%d\n", drawStatus);
} else if (sameColor(color, moveBtn)) {
/* 移动时设置drawStatus为3 */
drawStatus = 3;
printf("drawStatus:%d\n", drawStatus);
}

/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}else if (drawStatus == 2) {
/* 删除图形 */
deletePoly(x, y);
} else if(drawStatus == 3) {
moveNum = -1;
for (int i = 0; i< con; i++) {
if (pointInPoly(polygons[i], x, y)) {
moveNum = i;
}
}
}
}
}
}

好,现在就大功告成了,看一下运行结果吧

[embed]http://cdn.cuiqingcai.com/robot3.mp4[/embed]

现在完整的代码如下,代码过长,不完全显示,双击查看完整代码

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
#include <glut.h>
#include <math.h>
#include <stdio.h>
#include <iostream>

using namespace std;

#define PI 3.1415926
/* 每个图形最多的顶点数 */
#define MAX_VERTEX 10
/* 画的图形最多的个数 */
#define MAX_PLOY 10
/* 窗口长宽的一半 */
int halfWidth, halfHeight;
/* 绘制多边形的起始标志,0是开始绘制,1是结束绘制,初始为-1 */
int drawStatus = -1;
int moveNum = -1;
/* 定义点击点的x,y*/
int clickX, clickY;
/* 多边形结构体 */
struct polygon {
/* 顶点坐标 */
int x[MAX_VERTEX];
int y[MAX_VERTEX];
/* 定义第几个顶点 */
int verNum;
GLubyte color[3];
};
/* 各种颜色 */
GLubyte border[3] = { 0, 0, 0 };
GLubyte grey[3] = { 195, 195, 195 };
GLubyte yellow[3] = { 255, 243, 0 };
GLubyte red[3] = { 237, 28, 36 };
GLubyte darkGrey[3] = { 126, 126, 126 };
GLubyte white[3] = { 255, 255, 255 };
GLubyte startBtn[3] = { 10, 10, 10 };
GLubyte endBtn[3] = { 20, 20, 20 };
GLubyte delBtn[3] = { 30, 30, 30 };
GLubyte moveBtn[3] = { 40, 40, 40 };
/* 当前颜色 */
GLubyte nowColor[3] = { 0, 0, 0 };
/* 声明多边形数组 */
polygon polygons[MAX_PLOY];
/* 记录画了几个多边形 */
int con = 0;

/* 绘制多边形 */
void glPolygons()
{
if ( con >= 0 )
{
for ( int i = 0; i <= con; i++ )
{
/* 取到这个多边形 */
polygon poly = polygons[i];
/* 画封闭线 */
glBegin( GL_LINE_LOOP );
int num = poly.verNum;
printf( "num:%d\n", num );
for ( int j = 0; j < num; j++ )
{
glColor3ub( poly.color[0], poly.color[1], poly.color[2] );
glVertex2d( poly.x[j] - halfWidth, halfHeight - poly.y[j] );
printf( "polyx:%d,polyy:%d", poly.x[j], poly.y[j] );
}
/* 结束画线 */
glEnd();
/* 刷新 */
glFlush();
}
}
}


/* 绘制填充的圆形 */
void glColorCircle( int x, int y, int R, GLubyte color[3] )
{
/* 开始绘制曲线 */
glBegin( GL_POLYGON );
/* 设置颜色 */
glColor3ub( color[0], color[1], color[2] );
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for ( double i = 0; i <= 2 * PI; i += delta_angle )
{
/* 绝对定位加三角函数值 */
double vx = x + R * cos( i );
double vy = y + R*sin( i );
glVertex2d( vx, vy );
}
/* 结束绘画 */
glEnd();
glFlush();
}


/* 画矩形,传入的是左下角XY坐标和右上角XY坐标 */
void glRect( int leftX, int leftY, int rightX, int rightY )
{
/* 画封闭曲线 */
glBegin( GL_LINE_LOOP );
/* 左下角 */
glVertex2d( leftX, leftY );
/* 右下角 */
glVertex2d( rightX, leftY );
/* 右上角 */
glVertex2d( rightX, rightY );
/* 左上角 */
glVertex2d( leftX, rightY );
/* 结束画线 */
glEnd();
}


/* 画圆角矩形,传入矩形宽高,角半径,矩形中心点坐标 */
void glRoundRec( int centerX, int centerY, int width, int height, float cirR )
{
/* 二分之PI,一个象限的角度 */
float PI_HALF = PI / 2;
/* 划分程度,值越大画得越精细 */
float divide = 20.0;
/* 圆角矩形的坐标 */
float tx, ty;
/* 画封闭曲线 */
glBegin( GL_LINE_LOOP );
/* 四个象限不同的操作符 */
int opX[4] = { 1, -1, -1, 1 };
int opY[4] = { 1, 1, -1, -1 };
/* 用来计数,从第一象限到第四象限 */
float x = 0;
/* x自增时加的值 */
float part = 1 / divide;
/* 计算内矩形宽高一半的数值 */
int w = width / 2 - cirR;
int h = height / 2 - cirR;
/* 循环画线 */
for ( x = 0; x < 4; x += part )
{
/* 求出弧度 */
float rad = PI_HALF * x;
/* 计算坐标值 */
tx = cirR * cos( rad ) + opX[(int) x] * w + centerX;
ty = cirR * sin( rad ) + opY[(int) x] * h + centerY;
/*传入坐标画线 */
glVertex2f( tx, ty );
}
/* 结束画线 */
glEnd();
}


/* 画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径 */
void glArc( double x, double y, double start_angle, double end_angle, double radius )
{
/* 开始绘制曲线 */
glBegin( GL_LINE_STRIP );
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for ( double i = start_angle; i <= end_angle; i += delta_angle )
{
/* 绝对定位加三角函数值 */
double vx = x + radius * cos( i );
double vy = y + radius*sin( i );
glVertex2d( vx, vy );
}
/* 结束绘画 */
glEnd();
}


/* 画圆 */
void glCircle( double x, double y, double radius )
{
/* 画全圆 */
glArc( x, y, 0, 2 * PI, radius );
}


/* 画三角形,传入三个点的坐标 */
void glTri( int x1, int y1, int x2, int y2, int x3, int y3 )
{
/* 画封闭线 */
glBegin( GL_LINE_LOOP );
/* 一点 */
glVertex2d( x1, y1 );
/* 二点 */
glVertex2d( x2, y2 );
/*三点 */
glVertex2d( x3, y3 );
/* 结束画线 */
glEnd();
}


/* 画线,传入两点坐标 */
void glLine( int x1, int y1, int x2, int y2 )
{
/* 画封闭线 */
glBegin( GL_LINE_STRIP );
/* 一点 */
glVertex2d( x1, y1 );
/* 二点 */
glVertex2d( x2, y2 );
/* 结束画线 */
glEnd();
}


/* 函数用来画图 */
void display( void )
{
/* GL_COLOR_BUFFER_BIT表示清除颜色 */
glClear( GL_COLOR_BUFFER_BIT );
/* 设置画线颜色 */
glColor3f( 0.5, 0.5, 0.5 );
/* 画点大小 */
glPointSize( 2 );
/* 画圆角矩形,大肚子 */
glRoundRec( 0, 0, 146, 120, 15 );
/* 画圆,中间小圈 */
glCircle( 0, 0, 10 );
/* 画矩形,脖子 */
glRect( -25, 60, 25, 76 );
/* 画圆角矩形,大脸 */
glRoundRec( 0, 113, 128, 74, 10 );
/* 两个眼睛 */
glCircle( -30, 111, 10 );
glCircle( 30, 111, 10 );
/* 两条天线 */
glLine( -35, 150, -35, 173 );
glLine( 35, 150, 35, 173 );
/* 圆角矩形,两个耳朵 */
glRoundRec( 81, 115, 20, 34, 5 );
glRoundRec( -81, 115, 20, 34, 5 );
/* 圆弧,画嘴 */
glArc( 0, 133, 11 * PI / 8, 13 * PI / 8, 45 );
/* 画三角,肚子里的三角 */
glTri( -30, -15, 30, -15, 0, 28 );
/* 画矩形,胳膊连接处 */
glRect( -81, 43, -73, 25 );
glRect( 81, 43, 73, 25 );
/* 画矩形,上臂 */
glRect( -108, 45, -81, 0 );
glRect( 108, 45, 81, 0 );
/* 画矩形,中臂 */
glRect( -101, 0, -88, -4 );
glRect( 101, 0, 88, -4 );
/* 画矩形,下臂 */
glRect( -108, -4, -81, -37 );
glRect( 108, -4, 81, -37 );
/* 画圆形,手掌 */
glCircle( -95, -47, 10 );
glCircle( 95, -47, 10 );

/* 绘制取色盘 */
glColorCircle( -280, 280, 10, red );
glColorCircle( -250, 280, 10, yellow );
glColorCircle( -220, 280, 10, grey );
glColorCircle( -180, 280, 10, nowColor );
glColorCircle( 250, 280, 10, startBtn );
glColorCircle( 280, 280, 10, endBtn );
glColorCircle( 220, 280, 10, delBtn );
glColorCircle( 190, 280, 10, moveBtn );
/* 保证前面的OpenGL命令立即执行,而不是让它们在缓冲区中等待 */
/* 绘制多边形 */
glPolygons();
glFlush();
}


/* 窗口大小变化时调用的函数 */
void ChangeSize( GLsizei w, GLsizei h )
{
/* 避免高度为0 */
if ( h == 0 )
{
h = 1;
}
/* 定义视口大小,宽高一致 */
glViewport( 0, 0, w, h );
int half = 300;
/* 定义宽高的一半 */
halfHeight = 300;
halfWidth = 300;
/* 重置坐标系统,使投影变换复位 */
glMatrixMode( GL_PROJECTION );
/* 将当前的用户坐标系的原点移到了屏幕中心 */
glLoadIdentity();
/* 定义正交视域体 */
if ( w < h )
{
/* 如果高度大于宽度,则将高度视角扩大,图形显示居中 */
glOrtho( -half, half, -half * h / w, half * h / w, -half, half );
} else {
/* 如果宽度大于高度,则将宽度视角扩大,图形显示居中 */
glOrtho( -half * w / h, half * w / h, -half, half, -half, half );
}
}


/* 判断两个颜色是否相等 */
bool sameColor( GLubyte color1[3], GLubyte color2[3] )
{
if ( color1[0] - color2[0] < 5 && color1[1] - color2[1] < 5 && color1[2] - color2[1] < 5 )
{
return(true);
} else {
return(false);
}
}


/* 画点 */
void glPoints( int x, int y )
{
glBegin( GL_POINTS );
/* 点直接设置为黑色 */
glColor3ub( 0, 0, 0 );
glVertex2d( x - halfWidth, halfHeight - y );
glEnd();
glFlush();
}


/* 判断一个点是否在多边形内部 */
bool pointInPoly( polygon poly, int x, int y )
{
int nCross = 0;
for ( int i = 0; i < poly.verNum; i++ )
{
int p1x = poly.x[i];
int p1y = poly.y[i];
int p2x = poly.x[(i + 1) % poly.verNum];
int p2y = poly.y[(i + 1) % poly.verNum];
/* 平行或在延长线上,忽略 */
if ( p1y == p2y || y < min( p1y, p2y ) || y > max( p1y, p2y ) )
{
continue;
}
/* 计算交点横坐标 */
double m = (double) (y - p1y) * (double) (p2x - p1x) / (double) (p2y - p1y) + p1x;
if ( m > x )
nCross++;
}
return(nCross % 2 == 1);
}


/* 移动多边形 */
void movePoly( int x, int y )
{
/* 取到要移动的图形 */
int verNum = polygons[moveNum].verNum;
printf( "verNum%d,x%d,y%d,clickX%d,clickY%d\n", verNum, x, y, clickX, clickY );
for ( int i = 0; i < verNum; i++ )
{
polygons[moveNum].x[i] += (x - clickX);
polygons[moveNum].y[i] += (y - clickY);
printf( "polyx%d\n", polygons[moveNum].x[i] );
}
/* 移动完毕之后重新记录当前的点 */
clickX = x;
clickY = y;
/* 重新绘制 */
glutPostRedisplay();
}


/* 删除多边形 */
void removePoly( int index )
{
if ( index > con )
{
printf( "Out Of Index\n" );
}
for ( int i = index; i < con; i++ )
{
polygons[i] = polygons[i + 1];
}
con--;
}


/* 删除多边形 */
void deletePoly( int x, int y )
{
int delenum = -1;
for ( int i = 0; i < con; i++ )
{
if ( pointInPoly( polygons[i], x, y ) )
{
delenum = i;
}
}
if ( delenum != -1 )
{
removePoly( delenum );
}
/* 重新绘制 */
glutPostRedisplay();
}


/*鼠标点击事件 */
void mouseClick( int btn, int state, int x, int y )
{
/* 赋值全局变量 */
clickX = x;
clickY = y;
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if ( btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN )
{
/* 如果在颜色选项卡中点击 */
if ( y < optionHeight && x < optionWidth )
{
glReadPixels( x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor );
printf( "r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2] );
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle( -180, 280, 10, nowColor );
/* 如果点击的是右侧选项按钮 */
}else if ( y < optionHeight && x > optionWidth )
{
/* 取当前的点 */
GLubyte color[3];
glReadPixels( x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color );
printf( "r:%d,g:%d,b:%d\n", color[0], color[1], color[2] );
/* 如果点击了开始绘制的按钮 */
if ( sameColor( color, startBtn ) )
{
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf( "drawStatus:%d\n", drawStatus );
/* 如果点击了结束绘制的按钮 */
} else if ( sameColor( color, endBtn ) )
{
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf( "drawStatus:%d\n", drawStatus );
} else if ( sameColor( color, delBtn ) )
{
/* 删除时设置drawStatus为2 */
drawStatus = 2;
printf( "drawStatus:%d\n", drawStatus );
} else if ( sameColor( color, moveBtn ) )
{
/* 移动时设置drawStatus为3 */
drawStatus = 3;
printf( "drawStatus:%d\n", drawStatus );
}

/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if ( drawStatus == 0 )
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints( x, y );
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf( "polyColor%d%d%d\n", polygons[con].color[0], polygons[con].color[1], polygons[con].color[2] );
polygons[con].verNum++;
printf( "con:%d,verNum:%d\n", con, polygons[con].verNum );
}else if ( drawStatus == 2 )
{
/* 删除图形 */
deletePoly( x, y );
} else if ( drawStatus == 3 )
{
moveNum = -1;
for ( int i = 0; i < con; i++ )
{
if ( pointInPoly( polygons[i], x, y ) )
{
moveNum = i;
}
}
}
}
}
}


/* 鼠标移动事件 */
void mouseMove( int x, int y )
{
if ( drawStatus == 3 && moveNum != -1 )
{
printf( "move x:%d,y:%dmoveNum:%d\n", x, y, moveNum );
movePoly( x, y );
}
}


/*程序入口 */
int main( int argc, char *argv[] )
{
/* 对GLUT进行初始化,并处理所有的命令行参数 */
glutInit( &argc, argv );
/* 指定RGB颜色模式和单缓冲窗口 */
glutInitDisplayMode( GLUT_RGB | GLUT_SINGLE );
/* 定义窗口的位置 */
glutInitWindowPosition( 100, 100 );
/* 定义窗口的大小 */
glutInitWindowSize( 600, 600 );
/* 创建窗口,同时为之命名 */
glutCreateWindow( "OpenGL" );
/* 设置窗口清除颜色为白色 */
glClearColor( 1.0f, 1.0f, 1.0f, 1.0f );
/* 参数为一个函数,绘图时这个函数就会被调用 */
glutDisplayFunc( &display );
/* 参数为一个函数,当窗口大小改变时会被调用 */
glutReshapeFunc( ChangeSize );
/*鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc( mouseClick );
/*鼠标移动事件,鼠标按下并移动时调用 */
glutMotionFunc( mouseMove );
/* 该函数让GLUT框架开始运行,所有设置的回调函数开始工作,直到用户终止程序为止 */
glutMainLoop();
/*程序返回 */
return(0);
}

以上,就可以实现图形的移动和删除了。

存盘和读盘

这个很简单,存盘我们只需要把所有图形的位置以及颜色输出到一个文本中,然后在读盘时,把文本中所有内容读入并存储到多边形结构体数组中,然后重新绘制即可。 代码的讲解直接写在注释中,比较好理解

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
54
55
56
57
58
/* 保存所有的图形 */
void savePolygons()
{
FILE *fp;
int i;
if ((fp = fopen("shapes.txt", "w")) == NULL)
{
printf("不能打开文件!");
return;
}
/* 保存一共多少个图形 */
fprintf(fp, "%d", con);
fprintf(fp, "\n");
for (i = 0; i < con; i++){
/* 保存颜色值 */
fprintf(fp, "%d %d %d ", polygons[i].color[0], polygons[i].color[1], polygons[i].color[2]);
/* 保存顶点数量 */
fprintf(fp, "%d ", polygons[i].verNum);
/* 保存顶点坐标 */
for (int j = 0; j < polygons[i].verNum; j++)
fprintf(fp, "%d ", polygons[i].x[j]);
for (int j = 0; j < polygons[i].verNum; j++)
fprintf(fp, "%d ", polygons[i].y[j]);
fprintf(fp, "\n");
}
fclose(fp);
printf("保存成功。\n");
}

/* 读取存档 */
void readPolygons()
{
FILE *fp;
int i;
if ((fp = fopen("shapes.txt", "r")) == NULL)
{
printf("不能打开文件!");
return;
}
fscanf(fp, "%d", &con);
printf("count%d",con);
fscanf(fp, "\n");
for (i = 0; i < con; i++){
/* 读取颜色值 */
fscanf(fp, "%d %d %d ", &polygons[i].color[0], &polygons[i].color[1], &polygons[i].color[2]);
/* 读取顶点数量 */
fscanf(fp, "%d ", &polygons[i].verNum);
/* 读取顶点坐标 */
for (int j = 0; j < polygons[i].verNum; j++)
fscanf(fp, "%d ", &polygons[i].x[j]);
for (int j = 0; j < polygons[i].verNum; j++)
fscanf(fp, "%d ", &polygons[i].y[j]);
fscanf(fp, "\n");
}
fclose(fp);
printf("读取成功。\n");

}

我们只需要再定义两个按钮,再 display 中画上这俩按钮

1
2
GLubyte storeBtn[3] = {50, 50, 50};
GLubyte loadBtn[3] = {55, 55, 55};

然后把 mouseClick 方法改写一下就好了。最终 mouseClick 方法如下

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/* 鼠标点击事件 */
void mouseClick( int btn, int state, int x, int y )
{
/* 赋值全局变量 */
clickX = x;
clickY = y;
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if ( btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN )
{
/* 如果在颜色选项卡中点击 */
if ( y < optionHeight && x < optionWidth )
{
glReadPixels( x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor );
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle( -180, 280, 10, nowColor );
/* 如果点击的是右侧选项按钮 */
}else if ( y < optionHeight && x > optionWidth )
{
/* 取当前的点 */
GLubyte color[3];
glReadPixels( x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color );
/* 如果点击了开始绘制的按钮 */
if ( sameColor( color, startBtn ) )
{
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
/* 如果点击了结束绘制的按钮 */
} else if ( sameColor( color, endBtn ) )
{
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
} else if ( sameColor( color, delBtn ) )
{
/* 删除时设置drawStatus为2 */
drawStatus = 2;
} else if ( sameColor( color, moveBtn ) )
{
/* 移动时设置drawStatus为3 */
drawStatus = 3;
} else if ( sameColor( color, storeBtn ) )
{
/* 存盘时设置drawStatus为4 */
drawStatus = 4;
glutPostRedisplay();
savePolygons();
} else if ( sameColor( color, loadBtn ) )
{
/* 读盘时设置drawStatus为5 */
drawStatus = 5;
readPolygons();
glutPostRedisplay();
}

/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if ( drawStatus == 0 )
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints( x, y );
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
polygons[con].verNum++;
}else if ( drawStatus == 2 )
{
/* 删除图形 */
deletePoly( x, y );
} else if ( drawStatus == 3 )
{
moveNum = -1;
for ( int i = 0; i < con; i++ )
{
if ( pointInPoly( polygons[i], x, y ) )
{
moveNum = i;
}
}
}
}
}
}

大功告成!现在只要你存盘成功,不管对图形进行怎样的移动,删除操作。只要再点击读盘,就可以恢复到存盘时的状态,是不是非常酷炫。 大家可以尝试一下!

总结

本篇文章说明了图形的删除,移动以及存盘读盘的功能。代码实现仅仅是我个人的思路,当然实现方式不止这一种,欢迎大家创新。 部分思路如果大家有更好的解决方法,欢迎同我交流,非常感谢。 总之希望对大家有帮助!小伙伴们加油!

C/C++

综述

在上一节我们学习了鼠标监听事件,在这里我们就利用它来做一个实例,对鼠标监听事件进行一个综合的应用。

要求

1. 绘制如下的机器人,并在此基础上进行创作 图片1 2. 对象创建:支持用户利用鼠标指定各个顶点位置,补充完整机器人的腿部。 3. 对象删除:支持用户选择一个腿部的多边形(与你的多边形保存的数据结构有关)并删除。 4. 对象存储:设计一种数据结构存储每个多边形的顶点与边,支持文件存盘,并支持读盘复原。 5. 支持用户选择多边形的颜色,支持用户移动多边形

成果

在这里为了动态演示,将我的成果以视频形式演示如下

[embed]http://cdn.cuiqingcai.com/robot.mp4[/embed]

好,欣赏完视频之后是不是非常想知道是怎样实现的呢?下面我们来一步一步进行讲解。

绘制半机器人

在前面的几篇中,已经详细描述了绘制机器人的方法,在这里我们简单贴一下代码来绘制一个半机器人。 直接上代码吧,不多说了

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#include <glut.h>
#include <math.h>
#define PI 3.1415926

/* 画矩形,传入的是左下角XY坐标和右上角XY坐标 */
void glRect( int leftX, int leftY, int rightX, int rightY )
{
/* 画封闭曲线 */
glBegin( GL_LINE_LOOP );
/* 左下角 */
glVertex2d( leftX, leftY );
/* 右下角 */
glVertex2d( rightX, leftY );
/* 右上角 */
glVertex2d( rightX, rightY );
/* 左上角 */
glVertex2d( leftX, rightY );
/* 结束画线 */
glEnd();
}


/* 画圆角矩形,传入矩形宽高,角半径,矩形中心点坐标 */
void glRoundRec( int centerX, int centerY, int width, int height, float cirR )
{
/* 二分之PI,一个象限的角度 */
float PI_HALF = PI / 2;
/* 划分程度,值越大画得越精细 */
float divide = 20.0;
/* 圆角矩形的坐标 */
float tx, ty;
/* 画封闭曲线 */
glBegin( GL_LINE_LOOP );
/* 四个象限不同的操作符 */
int opX[4] = { 1, -1, -1, 1 };
int opY[4] = { 1, 1, -1, -1 };
/* 用来计数,从第一象限到第四象限 */
float x = 0;
/* x自增时加的值 */
float part = 1 / divide;
/* 计算内矩形宽高一半的数值 */
int w = width / 2 - cirR;
int h = height / 2 - cirR;
/* 循环画线 */
for ( x = 0; x < 4; x += part )
{
/* 求出弧度 */
float rad = PI_HALF * x;
/* 计算坐标值 */
tx = cirR * cos( rad ) + opX[(int) x] * w + centerX;
ty = cirR * sin( rad ) + opY[(int) x] * h + centerY;
/*传入坐标画线 */
glVertex2f( tx, ty );
}
/* 结束画线 */
glEnd();
}


/* 画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径 */
void glArc( double x, double y, double start_angle, double end_angle, double radius )
{
/* 开始绘制曲线 */
glBegin( GL_LINE_STRIP );
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for ( double i = start_angle; i <= end_angle; i += delta_angle )
{
/* 绝对定位加三角函数值 */
double vx = x + radius * cos( i );
double vy = y + radius*sin( i );
glVertex2d( vx, vy );
}
/* 结束绘画 */
glEnd();
}


/* 画圆 */
void glCircle( double x, double y, double radius )
{
/* 画全圆 */
glArc( x, y, 0, 2 * PI, radius );
}


/* 画三角形,传入三个点的坐标 */
void glTri( int x1, int y1, int x2, int y2, int x3, int y3 )
{
/* 画封闭线 */
glBegin( GL_LINE_LOOP );
/* 一点 */
glVertex2d( x1, y1 );
/* 二点 */
glVertex2d( x2, y2 );
/*三点 */
glVertex2d( x3, y3 );
/* 结束画线 */
glEnd();
}


/* 画线,传入两点坐标 */
void glLine( int x1, int y1, int x2, int y2 )
{
/* 画封闭线 */
glBegin( GL_LINE_STRIP );
/* 一点 */
glVertex2d( x1, y1 );
/* 二点 */
glVertex2d( x2, y2 );
/* 结束画线 */
glEnd();
}


/* 函数用来画图 */
void display( void )
{
/* GL_COLOR_BUFFER_BIT表示清除颜色 */
glClear( GL_COLOR_BUFFER_BIT );
/* 设置画线颜色 */
glColor3f( 0.5, 0.5, 0.5 );
/* 画点大小 */
glPointSize( 2 );
/* 画圆角矩形,大肚子 */
glRoundRec( 0, 0, 146, 120, 15 );
/* 画圆,中间小圈 */
glCircle( 0, 0, 10 );
/* 画矩形,脖子 */
glRect( -25, 60, 25, 76 );
/* 画圆角矩形,大脸 */
glRoundRec( 0, 113, 128, 74, 10 );
/* 两个眼睛 */
glCircle( -30, 111, 10 );
glCircle( 30, 111, 10 );
/* 两条天线 */
glLine( -35, 150, -35, 173 );
glLine( 35, 150, 35, 173 );
/* 圆角矩形,两个耳朵 */
glRoundRec( 81, 115, 20, 34, 5 );
glRoundRec( -81, 115, 20, 34, 5 );
/* 圆弧,画嘴 */
glArc( 0, 133, 11 * PI / 8, 13 * PI / 8, 45 );
/* 画三角,肚子里的三角 */
glTri( -30, -15, 30, -15, 0, 28 );
/* 画矩形,胳膊连接处 */
glRect( -81, 43, -73, 25 );
glRect( 81, 43, 73, 25 );
/* 画矩形,上臂 */
glRect( -108, 45, -81, 0 );
glRect( 108, 45, 81, 0 );
/* 画矩形,中臂 */
glRect( -101, 0, -88, -4 );
glRect( 101, 0, 88, -4 );
/* 画矩形,下臂 */
glRect( -108, -4, -81, -37 );
glRect( 108, -4, 81, -37 );
/* 画圆形,手掌 */
glCircle( -95, -47, 10 );
glCircle( 95, -47, 10 );
/* 保证前面的OpenGL命令立即执行,而不是让它们在缓冲区中等待 */
glFlush();
}


/* 窗口大小变化时调用的函数 */
void ChangeSize( GLsizei w, GLsizei h )
{
/* 避免高度为0 */
if ( h == 0 )
{
h = 1;
}
/* 定义视口大小,宽高一致 */
glViewport( 0, 0, w, h );
int half = 300;
/* 重置坐标系统,使投影变换复位 */
glMatrixMode( GL_PROJECTION );
/* 将当前的用户坐标系的原点移到了屏幕中心 */
glLoadIdentity();
/* 定义正交视域体 */
if ( w < h )
{
/* 如果高度大于宽度,则将高度视角扩大,图形显示居中 */
glOrtho( -half, half, -half * h / w, half * h / w, -half, half );
} else {
/* 如果宽度大于高度,则将宽度视角扩大,图形显示居中 */
glOrtho( -half * w / h, half * w / h, -half, half, -half, half );
}
}


/*程序入口 */
int main( int argc, char *argv[] )
{
/* 对GLUT进行初始化,并处理所有的命令行参数 */
glutInit( &argc, argv );
/* 指定RGB颜色模式和单缓冲窗口 */
glutInitDisplayMode( GLUT_RGB | GLUT_SINGLE );
/* 定义窗口的位置 */
glutInitWindowPosition( 100, 100 );
/* 定义窗口的大小 */
glutInitWindowSize( 600, 600 );
/* 创建窗口,同时为之命名 */
glutCreateWindow( "OpenGL" );
/* 设置窗口清除颜色为白色 */
glClearColor( 1.0f, 1.0f, 1.0f, 1.0f );
/* 参数为一个函数,绘图时这个函数就会被调用 */
glutDisplayFunc( &display );
/* 参数为一个函数,当窗口大小改变时会被调用 */
glutReshapeFunc( ChangeSize );
/* 该函数让GLUT框架开始运行,所有设置的回调函数开始工作,直到用户终止程序为止 */
glutMainLoop();
/*程序返回 */
return(0);
}

运行结果如下 20150511002257 这一步非常非常简单,如果有不懂的小伙伴请参见 此文章

对象创建

通过上面的一步,我们已经可以轻松地绘制出一个半身机器人,接下来我们就要通过监听鼠标事件来实现动态多边形的绘制了。 因为实验要求中要求了多边形的颜色绘制,所以颜色怎么选取呢?我是这么想的,在面板的左上角绘制几个色块,通过鼠标点击这几个色块,读取出 RGB 值,然后相应地设置当前绘图的颜色即可。 所以首先我在左上角定义了几个颜色色块,同时监听鼠标事件。 首先定义几个颜色的 RGB 值

1
2
3
4
5
6
7
8
9
/* 各种颜色 */
GLubyte border[3] = {0, 0, 0};
GLubyte grey[3] = {195, 195, 195};
GLubyte yellow[3] = {255, 243, 0};
GLubyte red[3] = {237, 28, 36};
GLubyte darkGrey[3] = {126, 126, 126};
GLubyte white[3] = {255, 255, 255};
/* 当前颜色 */
GLubyte nowColor[3] = {0, 0, 0};

然后定义一个绘制填充色的圆形的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 绘制填充的圆形 */
void glColorCircle(int x, int y, int R, GLubyte color[3])
{
/* 开始绘制曲线 */
glBegin(GL_POLYGON);
/* 设置颜色 */
glColor3ub(color[0], color[1], color[2]);
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for (double i = 0; i <= 2 * PI; i += delta_angle)
{
/* 绝对定位加三角函数值 */
double vx = x + R * cos(i);
double vy = y + R*sin(i);
glVertex2d(vx, vy);
}
/* 结束绘画 */
glEnd();
glFlush();
}

那么在 display 函数中,我们就可以调用下面的方法来绘制三个取色盘拾取颜色,其中最后一个取色盘保存了当前的颜色。

1
2
3
4
5
/* 绘制取色盘 */
glColorCircle(-280, 280, 10, red);
glColorCircle(-250, 280, 10, yellow);
glColorCircle(-220, 280, 10, grey);
glColorCircle(-180, 280, 10, nowColor);

这样光有了取色盘可不行,需要有鼠标点击事动态地改变取色的色值。现在我们可以加入鼠标监听方法如下 通过上一节的说明,我们定义一个 mouseClick 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {

}else{

}
}
}

另外加入监听方法

1
2
/*鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc(mouseClick);

在这里有几个地方需要说明一下。

1.选项卡的分界范围是什么?

其实我是把整个画面分割成了三部分,其中最上面 40 像素的一个长条就是选项卡,选项卡又分为了两部分。左边是取色板,右边是绘制的选项,比如取点,绘制,删除等等的操作。所以,optionHeight 就指的是上面的 40 像素的分割线,optionWidth 是中间的分割线,即上方调色板和绘制选项点的分割线。 通过鼠标点击判断是在哪个范围内从而响应不同的事件。 从而鼠标监听事件就是下面的结构了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {

/* 如果点击的是下方的绘图页面 */
}else{

}

其中第四个色板就是显示了当前的颜色,在-180,280 的位置绘制了当前的颜色色盘。并同时颜色值保存在了 nowColor 中。

2. halfHeight 和 halfWidth

这里定义了 halfHeight 和 halfWdith,在后面的绘图中会用到,同时在取色时也会用到,实际就是整个窗口宽高的一半,定义为全局变量即可。如当前的窗口是 600x600,所以现在二者就都是 300,300. 到现在,如果你按照流程走下来,代码是可以正常运行的,现在可以通过点击左上角的色盘进行取色,同时第四个色盘显示了当前的颜色。运行结果截图如下 20150511005807 好,接下来我们就进行多边形的定义了,多边形的定义我们利用结构体的形式,假设我们绘制的边数最大是 10,可以定义如下的多边形结构体,结构体中包含了顶点坐标(数组形式),顶点数量,颜色(RGB)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 每个图形最多的顶点数 */
#define MAX_VERTEX 10
/* 画的图形最多的个数 */
#define MAX_PLOY 10
/* 多边形结构体 */
struct polygon {
/* 顶点坐标 */
int x[MAX_VERTEX];
int y[MAX_VERTEX];
/* 定义第几个顶点 */
int verNum;
GLubyte color[3];
};
/* 声明多边形数组 */
polygon polygons[MAX_PLOY];

为了便于记录,我们定义一个 con 变量来记录当前绘制到了哪个多边形

1
2
/* 记录画了几个多边形 */
int con = 0;

好,定义好结构体之后,我们就可以先选取点,然后绘制多边形了。所以在这里我们定义两个按钮,取点和绘制。 定义两个颜色

1
2
GLubyte startBtn[3] = {10, 10, 10};
GLubyte endBtn[3] = {20, 20, 20};

我们可以用颜色来区分不同的按钮,现在原来的 display 函数中画上这俩按钮。

1
2
glColorCircle(250, 280, 10, startBtn);
glColorCircle(280, 280, 10, endBtn);

如图所示,当我们要取点的时候就可以点击一下左边的按钮,取完点后就可以点击右边的按钮完成绘制。 QQ截图20150511010802 在鼠标监听事件中,当点击了左边的按钮,便开始拾取几个顶点,然后点击右边的按钮之后,会将图形绘制出来。所以为了区分当前是在拾取点还是完成绘制,我们定义一个状态变量叫 drawStatus

1
2
/* 绘制多边形的起始标志,0是开始绘制,1是结束绘制,初始为-1 */
int drawStatus = -1;

在原来的 mouseClick 函数中,对于判断点击右上方选项卡的响应就可以改写如下

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
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
}
/* 如果点击的是下方的绘图页面 */
}else{

}
}
}

其中 sameColor 方法就是来判断当前点击的位置是否和按钮颜色相等,在这里为了避免误差,我们像往常一样设置一个容差。

1
2
3
4
5
6
7
8
9
10
/* 判断两个颜色是否相等 */
bool sameColor(GLubyte color1[3], GLubyte color2[3])
{
if (color1[0] - color2[0] < 5 && color1[1] - color2[1] < 5 && color1[2] - color2[1] < 5)
{
return(true);
} else {
return(false);
}
}

好,当点击了第一个黑色按钮(取点的按钮)时,我们便可以在下方拾取点来保存到数组中了。 在最后的 else 代码块中加入如下的方法,用来取点。 这里的取点是取到了点击的坐标值,当前的 RGB 值,每取一个点,响应的 verNum(顶点数目)就加一。 而且当前保存到的多边形是 polygons[con],con 初始值为 0,也就是保存到第一个多边形结构体中。 当我们点击了开始绘制按钮时,con 会加 1,并且将保存的多边形绘制出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}

到现在上面的 mouseClick 方法就变成了

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
54
55
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
}
/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}
}
}
}

在这里有几点说明如下

1.glPoints 方法

即画点方法,为了直观地显示用户选取了哪个点,我们设置了这个画点的方法,每点一个点,就在屏幕上显示出来。 方法定义如下

1
2
3
4
5
6
7
8
9
/* 画点 */
void glPoints(int x, int y) {
glBegin(GL_POINTS);
/* 点直接设置为黑色 */
glColor3ub(0, 0, 0);
glVertex2d(x - halfWidth, halfHeight - y);
glEnd();
glFlush();
}

在这里,glVertex2d 方法的参数为什么不是 x,y 呢?很简单,因为我们绘画时的坐标原点是窗口的中心点,而鼠标点击的坐标原点是左上角,为了做一个映射变换,x 需要减去 halfWidth,y 需要取反并加 halfWidth。总之定义这两个 half 变量还是用处多多的。

2.glutPostRedisplay

这个方法就是重新绘制的方法,我们看到在结束绘制的地方调用了这个方法,其实就是重新调用了 display 方法,所以为了让我们绘制的图形正常显示,我们需要在 display 方法中来绘制我们已经存储好的多边形。我们可以把绘制多边形定义为一个方法,然后在 display 中调用即可。 绘制多边形的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 绘制多边形 */
void glPolygons()
{
if (con>=0) {
for (int i = 0; i <= con; i++) {
/* 取到这个多边形 */
polygon poly = polygons[i];
/* 画封闭线 */
glBegin(GL_LINE_LOOP);
int num = poly.verNum;
printf("num:%d\n",num);
for (int j = 0; j < num; j++)
{
glColor3ub(poly.color[0], poly.color[1], poly.color[2]);
glVertex2d(poly.x[j] - halfWidth, halfHeight - poly.y[j]);
printf("polyx:%d,polyy:%d",poly.x[j],poly.y[j]);
}
/* 结束画线 */
glEnd();
/* 刷新 */
glFlush();
}
}
}

在 display 函数中调用一下即可,方法的最后面调用一下。

1
2
/* 绘制多边形 */
glPolygons();

好,现在我们点击第一个黑色圆点,然后在屏幕上取点,取完点之后,然后点击右上角的黑色圆点,即可完成绘制 运行结果如下 QQ截图20150511013259 在此先告一段落,下一篇我们接着这里来实现图形的删除,移动以及存盘,读盘等操作。

本节代码

到这里,提供如下代码,代码运行结果即为上图所示,由于代码太长,不完全展开显示,双击查看全部代码

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
#include <glut.h>
#include <math.h>
#include <stdio.h>

#define PI 3.1415926
/* 每个图形最多的顶点数 */
#define MAX_VERTEX 10
/* 画的图形最多的个数 */
#define MAX_PLOY 10
/* 窗口长宽的一半 */
int halfWidth, halfHeight;
/* 绘制多边形的起始标志,0是开始绘制,1是结束绘制,初始为-1 */
int drawStatus = -1;
/* 多边形结构体 */
struct polygon {
/* 顶点坐标 */
int x[MAX_VERTEX];
int y[MAX_VERTEX];
/* 定义第几个顶点 */
int verNum;
GLubyte color[3];
};
/* 各种颜色 */
GLubyte border[3] = {0, 0, 0};
GLubyte grey[3] = {195, 195, 195};
GLubyte yellow[3] = {255, 243, 0};
GLubyte red[3] = {237, 28, 36};
GLubyte darkGrey[3] = {126, 126, 126};
GLubyte white[3] = {255, 255, 255};
GLubyte startBtn[3] = {10, 10, 10};
GLubyte endBtn[3] = {20, 20, 20};
/* 当前颜色 */
GLubyte nowColor[3] = {0, 0, 0};
/* 声明多边形数组 */
polygon polygons[MAX_PLOY];
/* 记录画了几个多边形 */
int con = 0;

/* 绘制多边形 */
void glPolygons()
{
if (con>=0) {
for (int i = 0; i <= con; i++) {
/* 取到这个多边形 */
polygon poly = polygons[i];
/* 画封闭线 */
glBegin(GL_LINE_LOOP);
int num = poly.verNum;
printf("num:%d\n",num);
for (int j = 0; j < num; j++)
{
glColor3ub(poly.color[0], poly.color[1], poly.color[2]);
glVertex2d(poly.x[j] - halfWidth, halfHeight - poly.y[j]);
printf("polyx:%d,polyy:%d",poly.x[j],poly.y[j]);
}
/* 结束画线 */
glEnd();
/* 刷新 */
glFlush();
}
}
}

/* 绘制填充的圆形 */
void glColorCircle(int x, int y, int R, GLubyte color[3])
{
/* 开始绘制曲线 */
glBegin(GL_POLYGON);
/* 设置颜色 */
glColor3ub(color[0], color[1], color[2]);
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for (double i = 0; i <= 2 * PI; i += delta_angle)
{
/* 绝对定位加三角函数值 */
double vx = x + R * cos(i);
double vy = y + R*sin(i);
glVertex2d(vx, vy);
}
/* 结束绘画 */
glEnd();
glFlush();
}

//画矩形,传入的是左下角XY坐标和右上角XY坐标
void glRect(int leftX,int leftY,int rightX,int rightY){
//画封闭曲线
glBegin(GL_LINE_LOOP);
//左下角
glVertex2d(leftX,leftY);
//右下角
glVertex2d(rightX,leftY);
//右上角
glVertex2d(rightX,rightY);
//左上角
glVertex2d(leftX,rightY);
//结束画线
glEnd();
}

//画圆角矩形,传入矩形宽高,角半径,矩形中心点坐标
void glRoundRec(int centerX,int centerY,int width,int height,float cirR){
//二分之PI,一个象限的角度
float PI_HALF = PI/2;
//划分程度,值越大画得越精细
float divide=20.0;
//圆角矩形的坐标
float tx,ty;
//画封闭曲线
glBegin(GL_LINE_LOOP);
//四个象限不同的操作符
int opX[4]={1,-1,-1,1};
int opY[4]={1,1,-1,-1};
//用来计数,从第一象限到第四象限
float x=0;
//x自增时加的值
float part=1/divide;
//计算内矩形宽高一半的数值
int w=width/2-cirR;
int h=height/2-cirR;
//循环画线
for(x=0;x<4;x+=part){
//求出弧度
float rad = PI_HALF*x;
//计算坐标值
tx=cirR*cos(rad)+opX[(int)x]*w+centerX;
ty=cirR*sin(rad)+opY[(int)x]*h+centerY;
//传入坐标画线
glVertex2f(tx,ty);
}
//结束画线
glEnd();
}

//画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径
void glArc(double x,double y,double start_angle,double end_angle,double radius)
{
//开始绘制曲线
glBegin(GL_LINE_STRIP);
//每次画增加的弧度
double delta_angle=PI/180;
//画圆弧
for (double i=start_angle;i<=end_angle;i+=delta_angle)
{
//绝对定位加三角函数值
double vx=x+radius * cos(i);
double vy=y+radius*sin(i);
glVertex2d(vx,vy);
}
//结束绘画
glEnd();
}


//画圆
void glCircle(double x, double y, double radius)
{
//画全圆
glArc(x,y,0,2*PI,radius);
}

//画三角形,传入三个点的坐标
void glTri(int x1,int y1,int x2,int y2,int x3,int y3){
//画封闭线
glBegin(GL_LINE_LOOP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//三点
glVertex2d(x3,y3);
//结束画线
glEnd();
}

//画线,传入两点坐标
void glLine(int x1,int y1,int x2,int y2){
//画封闭线
glBegin(GL_LINE_STRIP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//结束画线
glEnd();
}

//函数用来画图
void display(void)
{
//GL_COLOR_BUFFER_BIT表示清除颜色
glClear(GL_COLOR_BUFFER_BIT);
//设置画线颜色
glColor3f(0.5,0.5,0.5);
//画点大小
glPointSize(2);
//画圆角矩形,大肚子
glRoundRec(0,0,146,120,15);
//画圆,中间小圈
glCircle(0,0,10);
//画矩形,脖子
glRect(-25,60,25,76);
//画圆角矩形,大脸
glRoundRec(0,113,128,74,10);
//两个眼睛
glCircle(-30,111,10);
glCircle(30,111,10);
//两条天线
glLine(-35,150,-35,173);
glLine(35,150,35,173);
//圆角矩形,两个耳朵
glRoundRec(81,115,20,34,5);
glRoundRec(-81,115,20,34,5);
//圆弧,画嘴
glArc(0,133,11*PI/8,13*PI/8,45);
//画三角,肚子里的三角
glTri(-30,-15,30,-15,0,28);
//画矩形,胳膊连接处
glRect(-81,43,-73,25);
glRect(81,43,73,25);
//画矩形,上臂
glRect(-108,45,-81,0);
glRect(108,45,81,0);
//画矩形,中臂
glRect(-101,0,-88,-4);
glRect(101,0,88,-4);
//画矩形,下臂
glRect(-108,-4,-81,-37);
glRect(108,-4,81,-37);
//画圆形,手掌
glCircle(-95,-47,10);
glCircle(95,-47,10);

/* 绘制取色盘 */
glColorCircle(-280, 280, 10, red);
glColorCircle(-250, 280, 10, yellow);
glColorCircle(-220, 280, 10, grey);
glColorCircle(-180, 280, 10, nowColor);
glColorCircle(250, 280, 10, startBtn);
glColorCircle(280, 280, 10, endBtn);
//保证前面的OpenGL命令立即执行,而不是让它们在缓冲区中等待
/* 绘制多边形 */
glPolygons();
glFlush();
}


//窗口大小变化时调用的函数
void ChangeSize(GLsizei w,GLsizei h)
{
//避免高度为0
if(h==0) {
h=1;
}
//定义视口大小,宽高一致
glViewport(0,0,w,h);
int half = 300;
/* 定义宽高的一半 */
halfHeight = 300;
halfWidth = 300;
//重置坐标系统,使投影变换复位
glMatrixMode(GL_PROJECTION);
//将当前的用户坐标系的原点移到了屏幕中心
glLoadIdentity();
//定义正交视域体
if(w<h) {
//如果高度大于宽度,则将高度视角扩大,图形显示居中
glOrtho(-half,half,-half*h/w,half*h/w,-half,half);
} else {
//如果宽度大于高度,则将宽度视角扩大,图形显示居中
glOrtho(-half*w/h,half*w/h,-half,half,-half,half);
}

}
/* 判断两个颜色是否相等 */
bool sameColor(GLubyte color1[3], GLubyte color2[3])
{
if (color1[0] - color2[0] < 5 && color1[1] - color2[1] < 5 && color1[2] - color2[1] < 5)
{
return(true);
} else {
return(false);
}
}

/* 画点 */
void glPoints(int x, int y) {
glBegin(GL_POINTS);
/* 点直接设置为黑色 */
glColor3ub(0, 0, 0);
glVertex2d(x - halfWidth, halfHeight - y);
glEnd();
glFlush();
}

/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
}
/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}
}
}
}




//程序入口
int main(int argc, char *argv[]){
//对GLUT进行初始化,并处理所有的命令行参数
glutInit(&argc, argv);
//指定RGB颜色模式和单缓冲窗口
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
//定义窗口的位置
glutInitWindowPosition(100, 100);
//定义窗口的大小
glutInitWindowSize(600, 600);
//创建窗口,同时为之命名
glutCreateWindow("OpenGL");
//设置窗口清除颜色为白色
glClearColor(1.0f,1.0f,1.0f,1.0f);
//参数为一个函数,绘图时这个函数就会被调用
glutDisplayFunc(&display);
//参数为一个函数,当窗口大小改变时会被调用
glutReshapeFunc(ChangeSize);
/*鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc(mouseClick);
//该函数让GLUT框架开始运行,所有设置的回调函数开始工作,直到用户终止程序为止
glutMainLoop();
//程序返回
return 0;
}

小伙伴们可以运行试试看。

综述

本节我们主要利用了鼠标时间来实现了取色,设置颜色,并成功通过取点绘制出了各种形状的多边形。

C/C++

综述

在前面的例子中我们绘制了 2D 机器人以及颜色的填充,另外还有平移放缩变换等等。不过这些绘图方式都有一个特点,那就是无法动态地响应事件,如果我们在例子中加入鼠标点击的响应时间,让图形根据鼠标点击的变化而变化,那将会是非常友好的。 那么这一篇我们就来看一下鼠标响应的实现。

鼠标点击

对于鼠标点击,无非是左右键,滚轮的点击以及区分按下还是按上。 监听方法如下: void mouseClick(int btn, int state, int x, int y) 方法名是任意的,参数有四个,分别是左右键,点击状态(按下还是松开),点击的 x,y 坐标。 那么我们在 main 函数中怎么调用,代码如下

1
2
/* 鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc(mouseClick);

我们利用了类库中提供的方法 glutMouseFunc() 这个方法用来监听鼠标事件,我们传入刚才定义的 mouseClick 函数,即可完成调用,同时 mouseClick 函数的参数都会被赋值。 通过上面的代码,在鼠标点击窗口内部时,会自动调用 mouseClick 方法,而且四个参数都会有赋值。可以直接使用这些值来操作。

鼠标移动

void mouseMove(int x, int y) 方法名我们也可以任意指定,参数有 2 个,分别是 x,y 在 main 函数中可以这样调用

1
2
/*鼠标移动事件,鼠标按下并移动时调用 */
glutMotionFunc(mouseMove);

我们利用了类库中提供的方法 glutMotionFunc() 同上面的原理,传入了 mouseMove 方法,在鼠标移动时,就可以对 mouseMove 方法的参数进行赋值。

实例

好了,上面我们说了两个鼠标事件的方法,下面我们用一个例子来感受一下

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
54
55
56
57
58
59
60
61
62
63
64
65
#include <glut.h>
#include <math.h>
#include <stdio.h>


/* 函数用来画图 */
void display(void)
{

}


/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
printf("点击了鼠标,btn:%d,state:%d,x:%d,y:%d\n", btn, state, x, y);
if(btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN){
printf("按下了左边按钮\n");
}
if(btn == GLUT_LEFT_BUTTON && state == GLUT_UP){
printf("松开了左边按钮\n");
}
if(btn == GLUT_RIGHT_BUTTON && state == GLUT_DOWN){
printf("按下了右边按钮\n");
}
if(btn == GLUT_RIGHT_BUTTON && state == GLUT_UP){
printf("松开了右边按钮\n");
}
}



/* 鼠标移动事件 */
void mouseMove(int x, int y)
{
printf("移动鼠标中,x:%d,y%d\n", x, y);
}


/* 程序入口 */
int main(int argc, char *argv[])
{
/* 对GLUT进行初始化,并处理所有的命令行参数 */
glutInit(&argc, argv);
/* 指定RGB颜色模式和单缓冲窗口 */
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
/* 定义窗口的位置 */
glutInitWindowPosition(100, 100);
/* 定义窗口的大小 */
glutInitWindowSize(400, 400);
/* 创建窗口,同时为之命名 */
glutCreateWindow("OpenGL");
/* 设置窗口清除颜色为白色 */
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
/* 参数为一个函数,绘图时这个函数就会被调用 */
glutDisplayFunc(&display);
/*鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc(mouseClick);
/*鼠标移动事件,鼠标按下并移动时调用 */
glutMotionFunc(mouseMove);
/* 该函数让GLUT框架开始运行,所有设置的回调函数开始工作,直到用户终止程序为止 */
glutMainLoop();
/*程序返回 */
return(0);
}

在上面的代码中,我们监听了鼠标点击和鼠标移动事件,并打印输出了鼠标点击事件的响应。 比如我先点击了鼠标左键,然后点击了鼠标右键。然后移动了鼠标左键,然后移动了鼠标右键。运行结果如下 0150511000009 可以发现它打印输出了鼠标的按钮代号,点击状态,点击的位置。 另外我们发现鼠标点击的坐标原点是左上角,这跟我们之前读取色值的坐标原点(左下角)不一样,也跟我们绘图的坐标原点(中心点)不一样。编程时一定要注意。 获取到了点击点的位置,我们就可以利用这些值来进行相应的作图处理了。 小伙伴们可以尝试一下。

综述

本节说的不多,主要是说明了鼠标监听事件的用法。glutMouseFunc 以及 glutMotionFunc,另外通过一个实例来演示了鼠标监听事件的用法,下一篇我们就利用这个知识来进行一个实例演示。大家加油!

HTML

综述

LESS 包含一套自定义的语法及一个解析器,用户根据这些语法定义自己的样式规则,这些规则最终会通过解析器,编译生成对应的 CSS 文件。LESS 并没有裁剪 CSS 原有的特性,更不是用来取代 CSS 的,而是在现有 CSS 语法的基础上,为 CSS 加入程序式语言的特性。 本文部分内容参考 官方文档

编译器

建议使用Koala编译器 下载地址

LESS 原理及使用方式

本质上,LESS 包含一套自定义的语法及一个解析器,用户根据这些语法定义自己的样式规则,这些规则最终会通过解析器,编译生成对应的 CSS 文件。LESS 并没有裁剪 CSS 原有的特性,更不是用来取代 CSS 的,而是在现有 CSS 语法的基础上,为 CSS 加入程序式语言的特性。下面是一个简单的例子:

清单 1. LESS 文件
1
2
3
4
5
6
7
8
@color: #4D926F; 

#header {
color: @color;
}
h2 {
color: @color;
}

经过编译生成的 CSS 文件如下:

清单 2. CSS 文件
1
2
3
4
5
6
#header { 
color: #4D926F;
}
h2 {
color: #4D926F;
}

从上面的例子可以看出,学习 LESS 非常容易,只要你了解 CSS 基础就可以很容易上手。 LESS 可以直接在客户端使用,也可以在服务器端使用。在实际项目开发中,我们更推荐使用第三种方式,将 LESS 文件编译生成静态 CSS 文件,并在 HTML 文档中应用。

客户端

我们可以直接在客户端使用 .less(LESS 源文件),只需要从 LESSCSS 下载 less.js 文件,然后在我们需要引入 LESS 源文件的 HTML 中加入如下代码:

1
<link rel="stylesheet/less" type="text/css" href="styles.less">

LESS 源文件的引入方式与标准 CSS 文件引入方式一样:

1
<link rel="stylesheet/less" type="text/css" href="styles.less">

需要注意的是:在引入 .less 文件时,rel 属性要设置为“stylesheet/less”。还有更重要的一点需要注意的是:LESS 源文件一定要在 less.js 引入之前引入,这样才能保证 LESS 源文件正确编译解析。

使用编译生成的静态 CSS 文件

我们可以通过 LESS 的编译器,将 LESS 文件编译成为 CSS 文件,在 HTML 文章中引入使用。这里要强调的一点,LESS 是完全兼容 CSS 语法的,也就是说,我们可以将标准的 CSS 文件直接改成 .less 格式,LESS 编译器可以完全识别。

语法

变量

LESS 允许开发者自定义变量,变量可以在全局样式中使用,变量使得样式修改起来更加简单。 我们可以从下面的代码了解变量的使用及作用:

清单 3. LESS 文件
1
2
3
4
5
@border-color : #b5bcc7; 

.mythemes tableBorder{
border : 1px solid @border-color;
}

经过编译生成的 CSS 文件如下:

清单 4. CSS 文件
1
2
3
.mythemes tableBorder { 
border: 1px solid #b5bcc7;
}

从上面的代码中我们可以看出,变量是 VALUE(值)级别的复用,可以将相同的值定义成变量统一管理起来。 该特性适用于定义主题,我们可以将背景颜色、字体颜色、边框属性等常规样式进行统一定义,这样不同的主题只需要定义不同的变量文件就可以了。当然该特性也同样适用于 CSS RESET(重置样式表),在 Web 开发中,我们往往需要屏蔽浏览器默认的样式行为而需要重新定义样式表来覆盖浏览器的默认行为,这里可以使用 LESS 的变量特性,这样就可以在不同的项目间重用样式表,我们仅需要在不同的项目样式表中,根据需求重新给变量赋值即可。 LESS 中的变量和其他编程语言一样,可以实现值的复用,同样它也有生命周期,也就是 Scope(变量范围,开发人员惯称之为作用域),简单的讲就是局部变量还是全局变量的概念,查找变量的顺序是先在局部定义中找,如果找不到,则查找上级定义,直至全局。下面我们通过一个简单的例子来解释 Scope。

清单 5. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
@width : 20px; 
#homeDiv {
@width : 30px;
#centerDiv{
width : @width;// 此处应该取最近定义的变量 width 的值 30px
}
}
#leftDiv {
width : @width; // 此处应该取最上面定义的变量 width 的值 20px

}

经过编译生成的 CSS 文件如下:

清单 6. CSS 文件
1
2
3
4
5
6
#homeDiv #centerDiv { 
width: 30px;
}
#leftDiv {
width: 20px;
}

Mixins(混入)

Mixins(混入)功能对用开发者来说并不陌生,很多动态语言都支持 Mixins(混入)特性,它是多重继承的一种实现,在 LESS 中,混入是指在一个 CLASS 中引入另外一个已经定义的 CLASS,就像在当前 CLASS 中增加一个属性一样。 我们先简单看一下 Mixins 在 LESS 中的使用:

清单 7. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个样式选择器
.roundedCorners(@radius:5px) {
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
// 在另外的样式选择器中使用
#header {
.roundedCorners;
}
#footer {
.roundedCorners(10px);
}

经过编译生成的 CSS 文件如下:

清单 8. CSS 文件
1
2
3
4
5
6
7
8
9
10
#header { 
-moz-border-radius:5px;
-webkit-border-radius:5px;
border-radius:5px;
}
#footer {
-moz-border-radius:10px;
-webkit-border-radius:10px;
border-radius:10px;
}

从上面的代码我们可以看出:Mixins 其实是一种嵌套,它允许将一个类嵌入到另外一个类中使用,被嵌入的类也可以称作变量,简单的讲,Mixins 其实是规则级别的复用。 Mixins 还有一种形式叫做 Parametric Mixins(混入参数),LESS 也支持这一特性:

清单 9. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个样式选择器
.borderRadius(@radius){
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
// 使用已定义的样式选择器
#header {
.borderRadius(10px); // 把 10px 作为参数传递给样式选择器
}
.btn {
.borderRadius(3px);// // 把 3px 作为参数传递给样式选择器

}

经过编译生成的 CSS 文件如下:

清单 10. CSS 文件
1
2
3
4
5
6
7
8
9
10
#header { 
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border-radius: 10px;
}
.btn {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
}

我们还可以给 Mixins 的参数定义一人默认值,如

清单 11. LESS 文件
1
2
3
4
5
6
7
8
.borderRadius(@radius:5px){ 
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
.btn {
.borderRadius;
}

经过编译生成的 CSS 文件如下:

清单 12. CSS 文件
1
2
3
4
5
.btn { 
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
}

像 JavaScript 中 arguments一样,Mixins 也有这样一个变量:@arguments。@arguments 在 Mixins 中具是一个很特别的参数,当 Mixins 引用这个参数时,该参数表示所有的变量,很多情况下,这个参数可以省去你很多代码。

清单 13. LESS 文件
1
2
3
4
5
6
7
8
.boxShadow(@x:0,@y:0,@blur:1px,@color:#000){ 
-moz-box-shadow: @arguments;
-webkit-box-shadow: @arguments;
box-shadow: @arguments;
}
#header {
.boxShadow(2px,2px,3px,#f36);
}

经过编译生成的 CSS 文件如下:

清单 14. CSS 文件
1
2
3
4
5
#header { 
-moz-box-shadow: 2px 2px 3px #FF36;
-webkit-box-shadow: 2px 2px 3px #FF36;
box-shadow: 2px 2px 3px #FF36;
}

Mixins 是 LESS 中很重要的特性之一,我们这里也写了很多例子,看到这些例子你是否会有这样的疑问:当我们拥有了大量选择器的时候,特别是团队协同开发时,如何保证选择器之间重名问题?如果你是 java 程序员或 C++ 程序员,我猜你肯定会想到命名空间 Namespaces,LESS 也采用了命名空间的方法来避免重名问题,于是乎 LESS 在 mixins 的基础上扩展了一下,看下面这样一段代码:

清单 15. LESS 文件
1
2
3
4
#mynamespace { 
.home {...}
.user {...}
}

这样我们就定义了一个名为 mynamespace 的命名空间,如果我们要复用 user 这个选择器的时候,我们只需要在需要混入这个选择器的地方这样使用就可以了。#mynamespace > .user。

嵌套的规则

在我们书写标准 CSS 的时候,遇到多层的元素嵌套这种情况时,我们要么采用从外到内的选择器嵌套定义,要么采用给特定元素加 CLASS 或 ID 的方式。在 LESS 中我们可以这样写:

清单 16. HTML 片段
1
2
3
4
5
6
7
<div id="home"> 
<div id="top">top</div>
<div id="center">
<div id="left">left</div>
<div id="right">right</div>
</div>
</div>
清单 17. LESS 文件
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
#home{ 
color : blue;
width : 600px;
height : 500px;
border:outset;
#top{
border:outset;
width : 90%;
}
#center{
border:outset;
height : 300px;
width : 90%;
#left{
border:outset;
float : left;
width : 40%;
}
#right{
border:outset;
float : left;
width : 40%;
}
}
}

经过编译生成的 CSS 文件如下:

清单 18. CSS 文件
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
#home { 
color: blue;
width: 600px;
height: 500px;
border: outset;
}
#home #top {
border: outset;
width: 90%;
}
#home #center {
border: outset;
height: 300px;
width: 90%;
}
#home #center #left {
border: outset;
float: left;
width: 40%;
}
#home #center #right {
border: outset;
float: left;
width: 40%;
}

从上面的代码中我们可以看出,LESS 的嵌套规则的写法是 HTML 中的 DOM 结构相对应的,这样使我们的样式表书写更加简洁和更好的可读性。同时,嵌套规则使得对伪元素的操作更为方便。

清单 19. LESS 文件
1
2
3
4
5
6
7
8
a { 
color: red;
text-decoration: none;
&:hover {// 有 & 时解析的是同一个元素或此元素的伪类,没有 & 解析是后代元素
color: black;
text-decoration: underline;
}
}

经过编译生成的 CSS 文件如下:

清单 20. CSS 文件
1
2
3
4
5
6
7
8
a { 
color: red;
text-decoration: none;
}
a:hover {
color: black;
text-decoration: underline;
}

运算及函数

在我们的 CSS 中充斥着大量的数值型的 value,比如 color、padding、margin 等,这些数值之间在某些情况下是有着一定关系的,那么我们怎样利用 LESS 来组织我们这些数值之间的关系呢?我们来看这段代码:

清单 21 . LESS 文件
1
2
3
4
5
@init: #111111; 
@transition: @init*2;
.switchColor {
color: @transition;
}

经过编译生成的 CSS 文件如下:

清单 22. CSS 文件
1
2
3
.switchColor { 
color: #222222;
}

上面的例子中使用 LESS 的 operation 是 特性,其实简单的讲,就是对数值型的 value(数字、颜色、变量等)进行加减乘除四则运算。同时 LESS 还有一个专门针对 color 的操作提供一组函数。下面是 LESS 提供的针对颜色操作的函数列表:

1
2
3
4
5
6
7
8
lighten(@color, 10%); // return a color which is 10% *lighter* than @color 
darken(@color, 10%); // return a color which is 10% *darker* than @color
saturate(@color, 10%); // return a color 10% *more* saturated than @color
desaturate(@color, 10%);// return a color 10% *less* saturated than @color
fadein(@color, 10%); // return a color 10% *less* transparent than @color
fadeout(@color, 10%); // return a color 10% *more* transparent than @color
spin(@color, 10); // return a color with a 10 degree larger in hue than @color
spin(@color, -10); // return a color with a 10 degree smaller hue than @color

PS: 上述代码引自 LESS CSS 官方网站,详情请见 http://lesscss.org/#-color-functions 使用这些函数和 JavaScript 中使用函数一样。

清单 23. LESS 文件
1
2
3
4
init: #f04615; 
#body {
background-color: fadein(@init, 10%);
}

经过编译生成的 CSS 文件如下:

清单 24. CSS 文件
1
2
3
#body { 
background-color: #f04615;
}

从上面的例子我们可以发现,这组函数像极了 JavaScript 中的函数,它可以被调用和传递参数。这些函数的主要作用是提供颜色变换的功能,先把颜色转换成 HSL 色,然后在此基础上进行操作,LESS 还提供了获取颜色值的方法,在这里就不举例说明了。 LESS 提供的运算及函数特性适用于实现页面组件特性,比如组件切换时的渐入渐出。

模式匹配

清单 25. LESS文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.tri{
width: 0;
height: 0;
overflow: hidden;
.triangle(bottom);
}

.triangle(top,@w:5px,@c:#CCC){
border-width: @w;
border-color: transparent transparent red transparent;
border-style: dashed dashed solid dashed;
}
.triangle(bottom,@w:5px,@c:#CCC){
border-width: @w;
border-color: red transparent transparent transparent;
border-style: solid dashed dashed dashed;
}
清单 26. CSS文件
1
2
3
4
5
6
7
8
.tri {
width: 0;
height: 0;
overflow: hidden;
border-width: 5px;
border-color: red transparent transparent transparent;
border-style: solid dashed dashed dashed;
}

通过以上可以看出,我们可以在LESS中定义相同名字的样式,第一个参数不同,在调用时可以通过传入第一个参数进行匹配,得到不同的样式即可。

避免编译

清单27. LESS文件
1
2
3
.test{
width:calc(300px-30px);
}
清单28. CSS文件
1
2
3
.test {
width: calc(270px);
}

有时候一些样式如果编译了之后就会产生不必要的麻烦,如果我们不想要编译,可以改写如下

清单29. LESS文件
1
2
3
.test{
width:~'calc(300px-30px)';
}
清单30. CSS文件
1
2
3
.test {
width: calc(300px-30px);
}

通过在前面加上一个 ~然后将内容用引号引起来,就可以避免编译了。

总结

本文提到的只是 LESS 的基本功能,更高级的功能如:字符串插值,服务器端使用配置,JavaScript 表达式,避免编译等可以参看 LESS 的官方网站。 LESS 以 CSS 语法为基础,又借用了很多我们熟知编程式语言的特性,这对于我们开发人员来讲学习成本几乎可以忽略,它在保留 CSS 语法的基础上扩展了更多实用的功能,LESS 为我们提供了一种新的编写样式表的方法,我们可以根据我们的项目特性选择使用 LESS 的部分特性,我们只需用很少的成本就可以换了很大的回报,一句话,Less is more,借助 LESS 可以更便捷的进行 Web 开发。

个人日记

等啊等,终于等到五一放假,这次必须要出去好好玩一番,解脱一下,释放一下。经过一系列思想斗争,最终决定去泰安方特了!昨天方特之旅结束之后,甚是爽快,在此记流水账一篇,不喜勿喷哈! 话说回来还是要先说五月二号啊,简直了,本来打算是在五月二号去的,带上室友小周周和隔壁的花花学长。一号那天我定了第二天的火车,早上 7 点半的火车,室友小周周也提前约好了出租车,准备早上六点出发去火车站。万事俱备只欠东风了,当天晚上早早滚去睡觉,定好五点半的闹钟,准备静静等待闹钟的响起。可是,可是,一切都是来得那么突然,第二天一大早起来竟然下雨了!泰安那边下雨了!天哪,睡之前我还特意看了看二号是多云啊!这不是坑爹吗?下雨就下雨吧,可是你不知道下雨方特室外项目是不开放的好嘛!顿时心都凉了。╮(╯▽╰)╭ 哎,改签吧,小周周给那位出租司机打电话,我去重新改签票。嗯,又一个坑爹的事情发生了,7 点之前网上购票是不开放的好嘛?发车前两小时是不能在网上改签和退票的好嘛?哎,算了,还好票便宜,才十二块五,索性我不要了!不要了!默默去买了三号的车票,特么的还是无座啊,无座。哎,多么痛的领悟,╮(╯▽╰)╭。 然后,然后,就默默等到了三号,还是五点半的闹钟,这次终于不下雨了!苍天啊!带上手机,耳机,money,学僧证,充电宝,六点出门坐上 TAXI,朝火车站奔去,这次是真快啊这速度,路上都没车的,一路狂奔,竟然六点半多点就到了火车站了,可是我们七点半的火车啊,这不是坑爹吗?于是乎,我们决定改签一下提前一点,有个七点稍多的一趟车,于是乎我们兴冲冲地跑向了改签大厅,惊呆了!怎么它就那么长的队伍,哦你,那么长,么长,长。试着排了排,二十分钟往前走了不到十步,也是醉了。哎,默默走掉,去候车室等着吧,七点半就七点半! 呼呼呼,检票咯,K771,我们走上站台,看到一条漫长的绿皮车,不是说好的无座吗?怎么这么多空座,我们三个找到一个就坐下来,不得不再次吐槽一下这种座位真难受。早上好困,睡觉睡觉,带上耳机听着歌,像打坐一样睡着了,就那么睡着了,我好佩服我自己。睡了一个小时,一睁眼,泰山站到了,不错不错,下车,出发。 出站了,果然不出我所料,好多人都拿着牌子,问去方特吗?免费接送,立即出发,同时卖门票,220 一位,其实方特门票就是 220 的,学生票可能就便宜 10 块钱。哎,这样就这样吧,他们是打着免费接送的名义,实质钱都包含在门票里面了。我们也担心方特人特别多,到时候买票排个时候,就直接跟着他走吧,反正也贵不了多少。 车是一个公交车,等人来齐了一起发车出发,我们几个竟然赶到了第一波,进去的时候车是空的,要等到其他人来才发车,这不是坑爹吗?坑爹吗?说好的立马出发的呢?算了,不计较这些了,其实等了也没多久就走了。在车上交了钱买了门票,那个收钱的人(姑且叫导游吧)跟我们讲了方特的各个注意事项,还是挺贴心的嘛!在这里透露给大家一点,记不住的我就记不住了。 包包最好要寄存起来,最好寄存在方特外面,比较便宜,当然也可以背着如果你不嫌沉的话。 贵重物品一定要看管好,方特里面丢的最多的就是数码相机,然后是手机,保管好了就行咯。 如果你不想一开始就玩惊险刺激的,建议从右边开始玩,如果你一开始就想玩刺激的,建议还是从右边开始玩哈哈哈。 项链啊首饰啊最好要收起来,要不然你想体验在空中翻滚的时候项链打到牙的感受吗? 哦,其他的忘了,我也不知道他说了多少了,可能比较健忘,记不起来还有啥了。后面的我就根据我的亲身体验来说了,哈哈。 走了半个多小时,终于到了!我们去存上包,买好雨衣以防神秘河谷的时候淋湿。正式进门!进门!冲冲冲!方特之旅正式开始!差不多九点半! 放个地图先 20150504150649 恩,进去看过去就是一个方特城堡,好有艺术气息的,旁边还放着欢乐的音乐,走在小路上,还有阳光照射着,哈哈不错。而且关键是路上人好少啊,哈哈,是不是五月三号外地的都回家了呢?是吗?后面你就知道了。 4d6577b5ga2451346b0fc&690&690 恩,我们没有进城堡玩,直接奔向右边的飞跃极限。噢丫,谁说的人少的?谁说的人少的?人都跑进来了,排队啊,一层、两层、三层…不数了,心好累。也就十来层吧。排个毛线,我们直接冲向了下一站,神秘河谷。PO 个图先 25540577,1681257058& 简直是人品好,我们进去的时候竟然排到了前面,是不是刚开放就被我们赶上了呢?而且,我们赶上了第一波。这次在外面买的雨衣派上用场了,穿上我们的半透明红色雨衣,带上蓝色高筒鞋套,上船。船一排四个人,一共好像是五排吧,我们很果断地选择了第一排,雨衣全部裹起来。准备冲,看到图了吗?这是一个小船从河谷里面二十米高空俯冲下来的样子,看那浪花。这次我们真正地感受到了它的魅力。河谷里面其实是有两个小下坡的,第一次比较小,杀杀你的锐气,第二次就是图中这个超大的下坡,从楼顶的那个位置冲下来,自己想象!第一次下坡还好,等到第二次下坡的时候,啊啊啊啊,二三十米高空俯冲,那种朝下的重力加速度深深牵引着我们,俯冲的时候我们叫喊着,咆哮着,嘶吼着,身心全部都感受着重力加速,咣!超大的浪花被我们激起来了,溅起十多米,丫的坑爹的是浪花不只是超前喷的啊,特么的它在空中划了一道优美的弧线直接想瓢泼一样泼到了我们身上啊,尤其是第一排啊第一排,最惨烈啊,尼玛我当初怎么就选了第一排啊,这不是坑爹吗?我真的是穿了雨衣,但是我屁股底下的雨衣不够长啊,水落到了座位上,然后水就浸到我的裤子里面去了啊,一阵凉意袭来,嗖嗖嗖,我屁股湿了,湿了。坑爹啊,这雨衣抵了个毛用啊。随后船缓缓划回去,出来的时候我走出来,屁股底下湿了一片,像尿了裤子一样,怎么能这样啊?我才玩了一个就湿了,湿了啊,╭(╯^╰)╮ 怎么这样。 还好太阳好,我就叉开腿走着,自己想象!赶紧的太阳把我的裤子晒干吧。接下来我们去了维苏威火山,听说这个还不错。果然,走到旁边就听到了火山里面一群人的嘶吼,开始排队吧,人也不大多啊,应该还是人品好的缘故吧,嗖嗖就排上了。PO 个图先 3e4c18d1g778440eee0a9&690 对,就是这么酷炫,这就是传说中的过山车,一个个小匣子接起来,我和花花坐在了一个匣子里,抓紧扶手,小车就开动了,呼呼呼,伴随着轮子的摩擦,伴随着我们的尖叫,在急转弯的地方,车呼啸而过,整个人有种呼之欲出的感觉,巨大的离心力简直爽到爆。一个又一个急转,一次又一次的钻洞,一声又一声的呐喊,一下又一下的心跳。自行想象! 出来之后旁边有个火流星,可是排队的人太多了,机智的我们就继续绕,看到有一个影视特技演出室刚开放,就跑进去了。尼玛一进去就是一个人在表演剁手,当然那刀是假的,duang,duang,duang,加了特效而已,后来欣赏了一点电影特效制作的过程,整体不算经验刺激,还说得过去吧。 后来继续绕,到了未来警察和生命之光那里,可惜的是都没有到开放的点,就去了星际航班,这下排队的人真多,到底什么鬼,干嘛的。我先 PO 个图 别看这个外面有个火箭什么的,这么酷炫,其实进去啊,我总算见识到了,排了半个小时队,最后二十个人坐在一个舱里,这个舱是可以动的,上下左右倾斜,可以模拟一下加速度什么的,然后整个前面就是一个超大的圆形幕布,像放电影一样,来给你模拟一个飞行的过程。随着画面的切换,舱左右摆动,也算是逼真。出来之后看到外面这个火箭,我本来还真以为要飞天呢,结果给我整这么一出,我也是醉醉的。 接下来就是恐龙危机了,一走过去吓我一跳啊,这是什么玩意翻了啊,上面的字怎么还是倒着的?好吧,经过缓过神来,不得不佩服这建筑真是溜啊,真是设计师逼死建筑师的典范啊,来来来,看图你就知道我为啥这么说了,尼玛这整个建筑都被恐龙掀翻了好嘛? XocG88iM553THvvg 虽然外面看着是倒着的,不过进去之后是正着的。呸,我这不废话么,倒着谁能走啊?进去也是想刚才那样,几个人坐在一个小车里,有个跑道,小车会在跑道上走,有时候转来转去。而四周当然就是恐龙咯,带上 4D 眼镜,能够看到周围银幕上一只只恐龙扑过来,小车上还有各种音效。小车有时候没油了,有时候加速了,有时候甩尾了,有时候弹飞了,模拟得非常逼真。特么的走到一个地方上面突然出现一个霸王龙头啊,这次不是屏幕出来的啊,是实物啊,一动一动的,它的牙像魔鬼的爪牙,还迈着魔鬼的步伐,在我不经意间粗线了啊,不能忍,特么的还长着嘴朝着我吼啊,吓尿了有木有。晃来晃去,这破小车终于抵达了传说中的安全地带,我也解脱出来了,╭(╯^╰)╮。臭霸王龙。 哎呦终于出来了,这也不能光玩室内的啊,来点室外的体验一把。于是乎,我们走到了一个叫波浪翻滚的东西那里,我了个去,这不就是济南泉城公园里面的遨游太空加强版吗? 遨游太空是这样子的 20150504202138 这里的波浪翻滚是这样子的,这何止是一样啊,这简直就是一样啊! 1278644413015_1_o 坐在那上面,就尽情地摇摆摇摆吧,飞上来,飞上去,有时候这玩意还是倾斜的啊,哦飞天的感觉真爽哈哈哈! 接下来就是传说中的大摆锤了,据说好多人都在这一关不行不行的了,上吐下泻的统统地都有。我记得还有人说买了一张票一开始就坐了个大摆锤,直接不行了,后面的项目都玩不了了,合着 220 块钱就坐了个这个,真值!我还是准备挑战一下,会会它,看看这玩意到底多么厉害。先 PO 图一张。 20131119GAig3 嗯,就是这个样子滴,它一边摆,一边转啊,最开始摆的角度是很小的,后来最高的时候都有 90 度了吧,我一边承受着离心力,一边又在半空中飘荡,简直有一种超凡脱俗的感觉。啊,当到达最高点的时候,我整个人横在了半空中同时围绕着中间的转轴在转啊,转啊。神马姿态,多么优美啊!我当时脑子里在想神马?我还活着吗?神呐。 最终,我还是活着下来了,然后又来了一个刺激的,体验蹦极的感觉。PO 图一张,先提前感受一下。 624188preview4 对,就是它!太空飞梭,哈哈哈哈,体验一下从 100 米高空坠落的感觉吧!上去坐在座位上绑好了,深呼吸一下,就在那一瞬间,就是那一瞬间,座位以一个超快的速度飞到架子的顶端啊!注意它不是缓缓上去的啊,是突然地一下,升到最上面,只需要短短几秒,那一瞬间啊,我的天,周围的人的呐喊,小周周的咆哮,花花的哭喊混成一团,就像火箭升空的感觉一样。然后,座位在空中停住了,尼玛这时候我们已经升到了最高的地方了啊。还不够形象,再来一张图!QQ截图20150504211315 将近一百米的高空,我们坐在座位上,腿是悬空的,朝下一瞅你可能就会昏死过去。然后,然后!这座位突然不受力了,直接自由落体!我们也跟随着座位飞下来,呐喊声,咆哮声混作一团,而我也在撕吼,啊啊啊啊。真正的自由落体,感受到了吗?而且它接下来还没完啊,又升上去,然后又来一次降下来。呼呼呼呼呼,简直了!!嗯,最后我还是活着下来了。 其实,这个还真的不够刺激,然后我们又来了一个刺激的,空中飞舞,哈哈哈,堪称一绝了。先 PO 个图感受一下。 简直了,这个巨大的翅上都坐着人,翅子这玩意不仅是自己在转,而且这个长臂在左右上下摇摆,尼玛可想而知我们的人在天上是波浪翻滚啊,朝上甩,朝下甩,朝左甩,朝右甩,有时倒这甩,有时正着甩。有没有玩过小时候的波浪鼓,我们这些人就想波浪鼓上面的两个圆球,各种甩,各种浪,在上面想不喊都不行,在上面我们的撕吼声简直是无与伦比,有时真的感觉自己就要被甩出去的样子,自己感受一下吧。这个简直是爽哭我了,花花直接吓得没敢坐,我和小周周真是感受到了这玩意的魅力,太浪了,也是浪到不行不行的。 接下来坐了坐勇敢者转盘,就是它!就是它!罪该万死的家伙!直接把我转晕了! 68a8f2b5-02d5-4fda-87cb-ae5fca294f04 别看他只是个转盘拉着一个个小车在那里转,其实它离心力特别大,我的腿在小车里简直都抬不起来。看到这个转盘下面的支撑臂了没?一开始它是平的啊,也就是说我们最开始是在水平面上转的,随着时间推移,这个臂慢慢往上升,升到将近 90 度的样子,尼玛我就简直被离心力牵引着在转啊,你可以想想我在最高处就是脚朝上头朝下在转啊转啊转啊转。而且离心力没那么大的话我还是斜着的好嘛?我的脖子也在不由自主地斜着,出来的时候真是转得我晕死了,脖子差点没正过来啊尼玛。 就是因为这个破玩意,我走路都开始晕了,趴桌子上一会,碎一觉,就没事了,起来接着浪! 恩,然后接下来就是飞越极限了,这个效果简直赞啊。排队的人虽然多,但是一次性进去 50 多个人一起飞。坐在椅子上,脚是悬空的,在眼前会有一个超大超大的银幕,先 PO 图一张。 W020140505433553648700 应该就是这种感觉吧,一排排人坐着,座位可以模拟飞行时的感受,比如模拟加速,左右转等等的动作,相当逼真,而且还吹着风,模拟一下风中飞行的感觉。当然最享受的还是眼前的超大的幕布,一幅幅 3D 效果的画面,有艾菲尔铁塔,金字塔,故宫,长城,布达拉宫,大峡谷,珠穆朗玛峰,东方明珠等等,风景特别多。在飞行的过程中带你穿越世界各地,游遍千山万水。实在是太棒,不得不赞一个。像的看了一个超大的电影偶也! 恩,最最最重头戏来了,火流星!号称是最惊险刺激的一个项目,如果你一开始坐了这个,其他的真的一点意思都没有,所以我们把这个放在了最后。来张图先感受一下。 20131216191455574 哈哈哈,害怕了吧,整个人都是坐在座位上脚是完全悬空的,图上的人在以一个超快的速度在做空中大反转,我估计他们差点就要被甩出去了。来张全图感受一下。 QQ截图20150504215457 恩,这跑道有正的有倒的,最高处差不多有三十四米。花花没有一起坐这个,我和小周周去尝试了。绑好之后,我们在那条长直道上慢慢地往上升,尼玛在升的过程中其实是比较慢的,十米,二十米,三十米,升的过程中我的脚下是悬空的,没有任何踩踏的地方,朝下一看,整个就是一大悬崖啊,啊啊啊。我想,一声我爱过这个世界足以表达我当时的感受。接着升到最高地方,就立即向下俯冲了,尼玛啊,简直就是自由落体一般的速度啊,而且这轨道它还不是一个平稳的正圆啊,跑道就像麻花一样向前延伸着,而我们就一边在自转一边向下俯冲,我的妈啊,全车的人都在喊叫,整个在穿梭的过程中肩膀被死死压着,一个巨大的扭转力扭动着我的身体,身体向前前进的同时在做大回环运动。这还不算完,降落到最低点之后接着又是一个上升,一个弧形的 360 度大回环跑道,那时我们的脚完全是在上方,头在下方,死死地卡在座位上,巨大的离心力给我的身体难以承受之重。图中那么长的跑道,跑完需要多久?2 分钟?1 分钟?不,只需要 30 秒!30 秒啊!那速度简直太快了,一个劲的酸爽,真能到达极限了。下来之后,感觉整个人都接受了全身的按摩,浑身的酥麻感觉,也是爽到不行不行的! 火流星,真是名副其实,就像太空穿梭一遍回来了一样,无法比拟的刺激。嗯,坐完这个心里就踏实了。哈哈。 后来就是几个比较放松的项目了,海螺湾,生命之光,转转杯,放松体验一下,最后爬了一下方特城堡,也算是圆满了。 晚上在方特门口拍了拍照,买了个西部牛仔帽子,留作纪念。方特之旅正式圆满结束!偶也! 最后 PO 图几张,留作纪念!真是开心!愉快! 再见!方特!再见!泰安!么么哒! QQ截图20150504221857 QQ截图20150504222004 QQ截图20150504222028QQ截图20150504221741

Other

综述

优秀的代码风格如同一身得体的打扮,能够给人以良好的印象。初学程序设计,首先必须建立良好的编程习惯,这其中就包括代码风格。本文就代码风格中的几个重点问题进行了讨论,并在文后给出了一份优秀的代码作为风格模板。代码风格不必花费太多专门的时间研究,在使用中不断模仿模板代码,轻轻松松就能写出“专业的代码”。

80字符,代码行极限

无论时空怎么转变,世界怎样改变,一行80字符应始终铭记心间。古老的Unix终端以80列的格式显示文本,为了让源代码与手册具有最佳的可读性, Unix系统始终坚持着80列的传统。80列不多不少,足够写出一行有意义的代码,同时也足够显示在终端屏幕,足够打印在A4纸上。虽然时至今日,我们的屏幕分辨率早已足够高,一行能够显示的内容远超超过80字符,但我们的优秀传统已经形成──几乎所有的Unix/Linux内核源代码以及联机用户手册都严格地遵守着80列极限。如果你正好在使用Windows平台下的Dev C++,你是否有注意到代码编辑框里那条细细的灰色竖线?不错,那正是代码行极限。除了HTML、XML等冗长繁复的标记式语言,几乎所有的语言都需要严格遵守代码行极限,这包括C、C++、Java、C#、Python、PHP等等。不过有时,比如当PHP跟HTML打交道的时候,这个限制是可以暂时放松的。过长的代码行总是不好的,好的代码要始终保持苗条的身材。

Tab还是Space,众说纷纭的缩进方式

代码离不开缩进,关于缩进主要有两个争论,一个是该用空格(Space)还是用制表符(Tab),另外一个是该用4格缩进还是8格缩进甚至都不是。 先来谈谈Space与Tab的问题。坚持用Space的程序员会告诉你,如果你从来都不用Tab,那么你的代码放到所有的地方看都是一样的。没错,这是用Space缩进的优点,可惜的是,这是它唯一的优点。代码层次越多,内层代码最前面的缩进便越多,这意味着你需要敲很多很多次空格。即使你能忍受不厌其烦地按空格键直到它坏掉,你也一定会被IDE总是自作聪明地插入一些Tab字符的行为烦恼不已。建议总是使用Tab缩进,因为几乎所有的代码(不仅仅是C 代码)都在使用Tab缩进。 Tab到底是4格还是8格?这是Tab缩进会被某些人诟病的根源。当你写程序时使用的Tab大小与别人读程序时使用的Tab大小不同时,再漂亮的排版也会变得杂乱无章。标准的Tab是8格的,而不幸的是,几乎所有的Windows平台下的IDE,包括 Visual Studio、Dev C++,甚至跨平台的Eclipse等,都默认使用4格Tab。我使用的FreeBSD系统的所有的内核源代码都采用8格缩进,所以我一直坚持使用8格缩进。也许你不习惯太大的间距,如果不是在Unix平台下,或者不是C语言,那就采用4格Tab吧。如果你在Unix下编写C代码,使用8格的标准Tab是更好的习惯。

折行原则,容易被忽略的角落

既然有代码行极限,很多情况下我们不得不断开一个完整的代码行,这就带来了一个问题:折行后应该如何缩进?好的做法是,第一次折行后,在原来缩进的基础上增加1/2的Tab大小的空格,之后的折行全部对齐第二行。可能这样的文字描述过于晦涩了,还是举个例子罢(以8格缩进为例):

1
2
3
4
5
6
7
if (value > a && value > b && value > c && value < d && value < e && value < f
value < h && value < h) { /* 注意折行后的缩进 */
value = value + 1;
value = value * value * value * value * value * value * value * value
* value * value + value * value * value * value * value * value
* value * value; /* 注意再次折行后的缩进 */
}

显然这个段代码没有任何实际用处,只是为了说明折行缩进而编造的。

无处不在的空格,无处不在的空行

需要空格的位置有:

  • if、while、switch等关键字与之后的左括号(之间。
  • 左花括号{之前。
  • 双目运算符两侧,例如p == NULL。
  • 逗号,与分号;之后,例如for (i = 0; i < 10; i++)。

不要空格的位置有:

  • 函数名与之后的左括号(,包括带参数的宏与之后的左括号(,例如max(a, b)。
  • 分号;与冒号:之前。
  • 左括号(右边,右括号)左边,例如if (p == NULL)。

需要空行的位置有:

  • 函数的定义之前、函数的定义之后
  • 一组联系紧密的代码段之前和之后

这些规则并不完全,当你碰到上面没有列举出来的情况时,请参考本文提供的模板代码。

左花括号的争议──换行乎?不换乎?

这又是一个仁者见仁智者见智的问题了。从使代码更清晰的角度看,作为代码段开头标识的左花括号{应该另起一行:

1
2
3
4
5
if (p == NULL)
{
printf("error!\n");
exit(0);
}

可是,这看起来实在不够紧凑,所以大部分的C代码(至少Unix上如此)都采用了这样的方式:

1
2
3
4
if (p == NULL) {
printf("error!\n");
exit(0);
}

我的建议是采用后者,这会使你的代码显得更加紧凑,也更加专业。需要说明一个特例,在定义函数时,我们总是要给左花括号 { 换行:

1
2
3
4
static int maxint(int a, int b)
{
return a > b ? a : b;
}

额,不过我还是不太习惯额

总结

本文介绍了编程的代码风格,希望对大家有帮助 本文内容为转载,原文出处

HTML

目前响应式布局和扁平化越来越风靡,前端 UI 框架也是风生水起,在这里分享几款优秀的前端 UI 框架

BootStrap

相比这个大家都听说过吧,Bootstrap,来自 Twitter,是目前最受欢迎的前端框架。Bootstrap 是基于 HTML、CSS、JAVASCRIPT 的,它简洁灵活,使得 Web 开发更加快捷。 20150427144535 官网链接 中文链接

Foundation

与 Bootstrap 相比,Foundation 屈居第二,不过这并不能说明它不受欢迎。 ZURB 在后面强有力的支持,使得 Foundation 更加强大!Foundation 被各大网站使用,如 Facebook、Mozilla、Ebay、 Yahoo、美国国家地理网站等等。 20150427154906 官网链接 中文链接

Semantic Ui

语义化设计的前端框架,为攻城师而制作的可复用的开源前端框架。 20150427155432 官网链接 中文链接

Amaze UI

Amaze UI 是一个移动优先的跨屏前端框架。… Amaze UI 以移动优先(Mobile first)为理念,从小屏逐步扩展到大屏,最终实现所有屏幕适配,适应移动互联潮流。 20150427155625 官网链接

Purecss

免费响应式 CSS 前端框架是有 Yahoo 发布的一个可以应用到所有 web 项目的小巧的、响应式的 CSS 模块集,Pure 是建立在 Normalize.css 的基础上的,对 HTML 元素提供了布局和样式,增加了一些公共的并且是你需要的 UI 组件。 20150427160204 官网链接 中文链接

JavaScript

综述

最近做项目的时候发现了一个非常奇怪的问题,就是对于click事件的响应。经过测试发现,对于IOS平台,直接监听click事件可能是没有响应的,而在Android和PC上则完全没有问题。所以通过获取设备信息实现了不同平台的不同监听。

IOS监听

对于IOS设备,只监听click方法可能是没有响应的。解决方法就是监听 “touchend click”事件。 而对于Android和PC,则只监听click事件即可。

平台检测

我们利用userAgent来检测平台

1
2
3
4
5
6
7
8
9
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
//alert(navigator.userAgent);
alert("iOS");
} else if (/(Android)/i.test(navigator.userAgent)) {
//alert(navigator.userAgent);
alert("Android");
} else {
alert("PC");
};

上面的JS代码可以检测三个平台。

实现监听

我们可以把方法自定义名字,比如

1
2
3
4
5
function back_click(){
$(".group-names").show();
$(".groups:visible").hide();
$(this).hide();
}

然后跨平台实现监听

1
2
3
4
5
6
/* bind the event */
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
$(".back").bind({"touchend click":back_click});
}else{
$(".back").bind({"click":back_click});
}

通过以上监听便没有问题了。

总结

通过以上方法便可以实现不同平台的监听。

JavaScript

综述

这里我们来利用jQuery实现瀑布流效果。

在线演示

我们首先来在线演示一下效果,然后我们说一下是怎样的实现,点开链接进行预览吧 在线预览

代码分析

首先我们在页面中加入了一些图片元素,都加到id为container的元素中,每一张图片都在一个class为box的容器里,然后里面是一张图片。

1
2
3
4
5
6
7
<div id="container">
<div class="box">
<div class="content">
<img src="img/1.jpg">
</div>
</div>
</div>

在JS中,我们遍历了每一个box,实现box的位置为上一行高度最小的box的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function imgLocation(){
var box = $(".box");
var boxWidth = box.eq(0).width();
var num = Math.floor($(window).width()/boxWidth);
var boxArr=[];
box.each(function(index,value){
var boxHeight = box.eq(index).height();
if(index<num){
boxArr[index]= boxHeight;
}else{
var minboxHeight = Math.min.apply(null,boxArr);
var minboxIndex = $.inArray(minboxHeight,boxArr);
$(value).css({
"position":"absolute",
"top":minboxHeight,
"left":box.eq(minboxIndex).position().left
});
boxArr[minboxIndex]+=box.eq(index).height();
}
});
}

最后实现了滚动条滑动时的动态加载

1
2
3
4
5
6
7
function scrollside(){
var box = $(".box");
var lastboxHeight = box.last().get(0).offsetTop+Math.floor(box.last().height()/2);
var documentHeight = $(document).width();
var scrollHeight = $(window).scrollTop();
return (lastboxHeight<scrollHeight+documentHeight)?true:false;
}

调用如下

1
2
3
4
5
6
7
8
9
10
window.onscroll = function(){
if(scrollside()){
$.each(dataImg.data,function(index,value){
var box = $("<div>").addClass("box").appendTo($("#container"));
var content = $("<div>").addClass("content").appendTo(box);
$("<img>").attr("src","./img/"+$(value).attr("src")).appendTo(content);
});
imgLocation();
}
};

通过上述简单的设置我们就实现了瀑布流的效果。

源码下载

源码下载

综述

以上便是瀑布流效果的实现,希望对大家有一定帮助!

C/C++

综述

OpenGL 中的变换可以分为下面的三种: 即模型变换、投影变换、视口变换。 模型变换其实就相当于图形的几何变换,包括平移、缩放、旋转等操作,下面我们来详细研究一下 OpenGL 中三种操作的函数应用。

准备工作

在变换前我们首先要做一下准备工作,首先我们需要调用 glMatrixMode 函数来设置变换模式。 该方法介绍如下 glMatrixMode(Glenum mode),设置当前矩阵模式,它具有三个参数,分别为GL_MODELVIEWGL_PROJECTIONGL_TEXTURE。三个参数的含义为:

GL_MODELVIEW,对模型视景矩阵堆栈应用随后的矩阵操作,也就是针对本节的模型几何变换设置的模式,本节所有内容均为此模式。 GL_PROJECTION,对投影矩阵应用随后的矩阵操作,在投影变换中设置的模式。 GL_TEXTURE,对纹理矩阵堆栈应用随后的矩阵操作,在设置动态纹理的过程中设置的模式。

所以,在本节我们就需要设置如下的模式

1
glMatrixMode(GL_MODELVIEW);

接下来,我们需要设置

1
glLoadIdentity();

这个方法的作用是设置将当前的用户坐标系的原点移到了屏幕中心,类似于一个复位操作。 好,设置好以上两个条件之后我们就可以来进行变换啦。主要有以下三个函数:

1
2
3
glRotatef(angle,vx,vy,vz) //绕向量(vx,vy,vz)为轴旋转角度angle
glTranslate(dx,dy,dz) //平移,位移量为向量(dx,dy,dz)
glScalef(sx,sy,sz) //比例缩放,x,y,z 方向的缩放因子分别为sx,sy,sz

下面我们来详细说明这三个函数的用法。

平移

三种变换中,平移变换是最简单的。 类库中提供了下面两个方法:

glTranslated(GLdouble x,GLdouble y,GLdouble z) glTranslatef(GLfloat x,GLfloat y,GLfloat z)

这两种方法是基本是等价的,一种方法是传入 double 类型的参数,另一个是传入 float 类型的参数。 方法的作用是将物体分别沿 x,y,z 轴平移 x,y,z 的单位长度。 利用上一节的机器人我们来感受一下

1.向右平移 100 像素

1
glTranslated(100,0,0);

运行结果 20150422142910 可以看到,机器人向右平移了 100 像素

2.向左下分别平移 50,50 像素

1
glTranslated(-50,-50,0);

运行结果 20150422143102 可以看到,机器人分别向左和向下平移了 50 像素

3.向左下分别平移 50,50,向前平移 100

1
glTranslated(-50,-50,100);

运行结果 20150422143102 哦,和上一个类似,也就是说在 z 轴上平移没有什么效果吗?难道因为我画的是二维平面图形?哦不,那不应该是距离我的视线更近了所以看到的更大了吗?好吧,事实好像不是这样的,我就姑且认为二维平面在 z 轴上的平移是没有影响的吧。 恩,这个函数基本用法就是这样,通过传入不同的 x,y,z 值实现平移即可。

旋转

旋转的方法有下面两个:

glRotatef(GLdouble angle,GLdouble x,GLdouble y,GLdouble z) glRotatef(GLfloat angle,GLfloat x,GLfloat y,GLfloat z)

其中第一个参数是旋转的角度,后面三个参数 x,yz 参数确立了旋转轴,旋转轴是原点(0,0,0)与(x,y,z)的连线。 下面我们用几个例子来感受一下

1.绕 x 轴旋转 60 度

1
glRotated(60,10,0,0);

运行结果 20150422144433 恩,它变扁了,旋转中心在机器人的中心,也就是中间的小红点,设想一下,绕 x 轴旋转 60 度,的确我们看到的应该就是变扁的机器人

2.绕 z 轴旋转 90 度

1
glRotated(90,0,0,10);

20150422144851 可以看到,机器人绕中心点旋转了 90 度,是逆时针方向。

3.旋转平移结合

感受完上面的两个例子之后,重头戏来了,上面的例子中旋转轴是(0,0,0)和(x,y,z)两点的连线,也就是旋转轴总是通过坐标原点的。 那如果我们要让物体绕特定的旋转轴来旋转,比如绕(1,2,3)和(4,5,6)来旋转怎么办? 这里就要用到平移和旋转的组合了。

如果旋转轴通过(xp,yp,zp),而不通过坐标原点。假如旋转轴为(xp,yp,zp)和(xq,yq,zq)确立的。我们首先要将图形沿 x,y,z 方向分别平移-xp,-yp,-zp,然后旋转参数(x,y,z)分别传入(xq-xp,yq-yp,zq-zp)三个值,旋转完毕之后再把图形沿 x,y,z 方向分别平移 xp,yp,zp 个单位长度即可。

在这里,我的教材和指导书中的内容又发生了冲突,教材中说的是先沿 x,y,z 方向分别平移-xp,-yp,-zp,再沿 x,y,z 方向分别平移xp,yp,zp还原。而指导书中则是先沿 x,y,z 方向分别平移xp,yp,zp,再沿 x,y,z 方向分别平移-xp,-yp,-zp还原,到底谁对谁错呢?我们来一个小例子验证一下。 如果觉得无聊,可以自行忽略下面小段内容。 为了直观地表示,我们统一设 z 坐标为 0,在 xy 平面中观察变换过程。 假设我们有一个点(2,2,0),旋转的轴我们设为(1,2,0)和(4,5,0)的连线,那么该点绕轴旋转 180 度之后应该会是(1,3,0),从图上可以很直观地看出 111 那么在调用类库的时候,我们首先就要对这个点进行平移,然后旋转,然后再反平移恢复。

首先,我们按照第一种说法,即先沿 x,y,z 方向分别平移-xp,-yp,-zp,即(2,2,0)减去(1,2,0),变为了(1,0,0),然后传入(xq-xp,yq-yp,zq-zp)实现绕(0,0,0)与(xq-xp,yq-yp,zq-zp)两点连线为旋转轴来旋转 180 度,显然是绕(0,0,0)与(3,3,0)的连线来旋转的,这个点变为了(0,1,0),然后再沿 x,y,z 方向分别平移 xp,yp,zp 还原,即加(1,2,0),即加得到的结果是(1,3,0)。确实与图中点的符合,所以这一种说法验证正确。 另一种说法,即先沿 x,y,z 方向分别平移 xp,yp,zp,即(2,2,0)加上(1,2,0),变为了(3,4,0),然后传入(xq-xp,yq-yp,zq-zp)实现绕(0,0,0)与(xq-xp,yq-yp,zq-zp)两点连线为旋转轴来旋转 180 度,显然是绕(0,0,0)与(3,3,0)的连线来旋转的,这个点变为了(4,3,0),然后再沿 x,y,z 方向分别平移-xp,-yp,-zp 还原,即减去(1,2,0),即加得到的结果是(3,1,0)。与图中的点不符,所以这一种说法验证失败。

可见,我们得到的结果是

如果旋转轴通过(xp,yp,zp),而不通过坐标原点。假如旋转轴为(xp,yp,zp)和(xq,yq,zq)确立的。我们首先要将图形沿 x,y,z 方向分别平移-xp,-yp,-zp,然后旋转参数(x,y,z)分别传入(xq-xp,yq-yp,zq-zp)三个值,旋转完毕之后再把图形沿 x,y,z 方向分别平移 xp,yp,zp 个单位长度即可。

所以,我们要实现机器人绕两点(10,20,30)和(40,50,60)定义的轴线旋转 45 度,代码实现如下

1
2
3
4
glTranslatef(-10,-20,-30);
//(40,50,60)-(10,20,30)=(30,30,30)
glRotatef(45,30,30,30);
glTranslatef(10,20,30);

运行结果 0150422151947 运行结果可能不太直观,可以直接参考代码实现

缩放

对于缩放,我们的方法也是有两个

glScaled(GLdouble x,GLdouble y,GLdouble z) glScalef(GLfloat x,GLfloat y,GLfloat z)

区别也就是一个参数为 double 类型,一个为 float 类型 函数的作用是将物体分别在 x,y,z 轴方向缩放为 x,y,z 倍,缩放中心为原点 下面我们来几个小例子感受一下

1.沿 x 轴放大 2 倍

1
glScaled(2,1,1);

运行结果 20150422153045 画面好美!

2.沿 y 轴 z 轴分别放大 2 倍

1
glScaled(1,2,2);

运行结果 20150422153914 因为整个图形是一个平面图,所以我们只可以看出 z 轴放缩的效果

3.放缩和平移的变换

和旋转的原理一样,默认的放缩中心是原点,在这里,如果我们的放缩中心如果不是原点,我们该怎样设置呢? 假设放缩中心为(xp,yp,zp),方法如下:

如果我们的放缩中心设置为(xp,yp,zp),而不是坐标原点。我们首先要将图形沿 x,y,z 方向分别平移-xp,-yp,-zp,然后放缩参数(x,y,z)分别传入三个值代表放缩比例,放缩完毕之后再把图形沿 x,y,z 方向分别平移 xp,yp,zp 个单位长度还原。

下面,我们以(50,50,50)为放缩中心来对机器人的 xy 方向分别放缩 1.5 倍

1
2
3
glTranslated(-50,-50,0);
glScaled(1.5,1.5,0);
glTranslated(50,50,0);

运行结果 20150422155459 这样,我们就实现了以(50,50,0)为放缩原点来进行放大 1.5 倍的操作。

综述

在本篇我们描述了 OpenGL 中的平移旋转放缩变换操作,以及一些实例演示,希望对大家有帮助!

C/C++

综述

上一节我们利用种子填充算法实现了机器人的区域填充,我们可以发现,种子填充需要一定是时间,并不能在第一时间填充完毕。这里我们介绍一种更加简单的方法,我们利用 glPolygonMode 来绘制机器人,感受一下。

注意

我们曾经分别使用过类库和直线圆弧生成算法来绘制过机器人的线条。 类库绘制法:OpenGL 绘图实例一之机器人的绘制 算法绘制法:OpenGL 绘图实例二之直线和圆弧的绘制 填充算法也有两种,一种是种子填充算法,另一种就是本篇说的方法。 在这里我们可以归一下类,算法绘制线条方法和种子填充算法中的每一个点都是我们一点点绘制的,都是我们利用算法一点点计算的坐标来绘制,因此,可以把他俩归为一对。而类库绘制线条方法和本篇说的填充方法都是指定一部分点,类库为我们实现了填充,因此可以把他俩归为一对。下面就让我们来感受一下类库填充的魅力吧!

函数说明

1.函数原型

函数原型:void glPolygonMode(GLenum face, GLenum mode); 说明:控制一个多边形的正面和背面的绘图模式。表示多边形应该被画成点、轮廓还是填充形式。在默认情况下,多边形的正面和背面都被画成填充形式。 函数 glPolygonMode()用于改变当前的多边形绘制模式,它有两个参数。第一个参数指定哪些面将被这个调用,可能的值为 GL_FRONT、GL_BACK 或 GL_FRONT_AND_BACK,分别对应于前向多边形、后向多边形或两者。第二个参数指定将要使用的模式,可以为 GL_POINT、GL_LINE 或 GL_FILL。 下面我们来用一个小 Demo 来感受一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glBegin(GL_TRIANGLES);
glVertex3f(‐0.8f,0.0f,0.0f);
glVertex3f(‐0.6f,0.0f,0.0f);
glVertex3f(‐0.7f,0.2f,0.0f);
glEnd();
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glBegin(GL_TRIANGLES);
glVertex3f(0.1f,0.0f,0.0f);
glVertex3f(‐0.1f,0.0f,0.0f);
glVertex3f(0.0f,0.2f,0.0f);
glEnd();
glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);
glBegin(GL_TRIANGLES);
glVertex3f(0.6f,0.0f,0.0f);
glVertex3f(0.8f,0.0f,0.0f);
glVertex3f(0.7f,0.2f,0.0f);
glEnd();

以上三种模式分别是绘制了填充的三角形,三角形的边界,三角形的三个点。

2.画多边形

在 glBegin 的参数传入 GL_POLYGON,绘制的时候所有的点就会连接而成一个多边形。

3.线条的粗细

1
glLineWidth(lineWidth);

利用这个方法,我们可以传入像素值大小,比如传入 2,画线的时候就是 2 像素的粗细。

绘制机器人

接下来我们利用这个方法来绘制机器人吧!我们可以注意到,定义了 glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)之后, 如果 glBegin 传入的是 GL_LINE_LOOP,绘制的仍然是封闭的曲线,如果 glBegin 传入的是 GL_POLYGON,绘制的则是封闭的填充的图形。 图片1 如果要实现上图的机器人,我们既要绘制黑色的边线,又要绘制带颜色的区块。所以我们就需要分别调用两种绘制模式,绘制的过程是完全相同的。所以,我们可以把模式当做一个变量来传入参数中改变绘制模式。比如之前的绘制三角形的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
//画三角形,传入三个点的坐标
void glTri(int x1,int y1,int x2,int y2,int x3,int y3){
//画封闭线
glBegin(GL_LINE_LOOP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//三点
glVertex2d(x3,y3);
//结束画线
glEnd();
}

就可以改写为

1
2
3
4
5
6
7
8
9
10
11
12
13
//画三角形,传入三个点的坐标
void glTri(int x1,int y1,int x2,int y2,int x3,int y3,int MODE){
//画封闭线
glBegin(MODE);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//三点
glVertex2d(x3,y3);
//结束画线
glEnd();
}

在调用的时候我们只需要分别传入 GL_LINE_LOOP 和 GL_POLYGON 就可以了。 所有之前的方法改写如下

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//画矩形,传入的是左下角XY坐标和右上角XY坐标
void glRect(int leftX,int leftY,int rightX,int rightY,int MODE){
//画封闭曲线
glBegin(MODE);
//左下角
glVertex2d(leftX,leftY);
//右下角
glVertex2d(rightX,leftY);
//右上角
glVertex2d(rightX,rightY);
//左上角
glVertex2d(leftX,rightY);
//结束画线
glEnd();
}

//画圆角矩形,传入矩形宽高,角半径,矩形中心点坐标
void glRoundRec(int centerX,int centerY,int width,int height,float cirR,int MODE){
//二分之PI,一个象限的角度
float PI_HALF = PI/2;
//划分程度,值越大画得越精细
float divide=20.0;
//圆角矩形的坐标
float tx,ty;
//画封闭曲线
glBegin(MODE);
//四个象限不同的操作符
int opX[4]={1,-1,-1,1};
int opY[4]={1,1,-1,-1};
//用来计数,从第一象限到第四象限
float x=0;
//x自增时加的值
float part=1/divide;
//计算内矩形宽高一半的数值
int w=width/2-cirR;
int h=height/2-cirR;
//循环画线
for(x=0;x<4;x+=part){
//求出弧度
float rad = PI_HALF*x;
//计算坐标值
tx=cirR*cos(rad)+opX[(int)x]*w+centerX;
ty=cirR*sin(rad)+opY[(int)x]*h+centerY;
//传入坐标画线
glVertex2f(tx,ty);
}
//结束画线
glEnd();
}

//画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径
void glArc(double x,double y,double start_angle,double end_angle,double radius,int MODE)
{
//开始绘制曲线
glBegin(MODE);
//每次画增加的弧度
double delta_angle=PI/180;
//画圆弧
for (double i=start_angle;i<=end_angle;i+=delta_angle)
{
//绝对定位加三角函数值
double vx=x+radius * cos(i);
double vy=y+radius*sin(i);
glVertex2d(vx,vy);
}
//结束绘画
glEnd();
}


//画圆
void glCircle(double x, double y, double radius,int MODE)
{
//画全圆
glArc(x,y,0,2*PI,radius,MODE);
}

//画三角形,传入三个点的坐标
void glTri(int x1,int y1,int x2,int y2,int x3,int y3,int MODE){
//画封闭线
glBegin(MODE);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//三点
glVertex2d(x3,y3);
//结束画线
glEnd();
}

//画线,传入两点坐标
void glLine(int x1,int y1,int x2,int y2){
//画封闭线
glBegin(GL_LINE_STRIP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//结束画线
glEnd();
}

那么我们实现了上面的方法之后。比如我现在要画一个矩形,就要传入 GL_LINE_LOOP 绘制一个边缘,然后再传入 GL_POLYGON 绘制一个填充的矩形,其中这个矩形的长宽等于边缘的长宽减去线的宽度(经过探索发现,应该减去线宽的一半才对,因为线的基准点是在线宽的中心的) 所以,我们可以定义一个画带边矩形的方法,比如

1
2
3
4
5
6
7
//画填充的矩形,传入左上角和右下角的坐标
void glFillRect(int leftX,int leftY,int rightX,int rightY,int color[3]){
glColor3ub(border[0],border[1],border[2]);
glRect(leftX,leftY,rightX,rightY,GL_LINE_LOOP);
glColor3ub(color[0],color[1],color[2]);
glRect(leftX+lineWidth/2,leftY-lineWidth/2,rightX-lineWidth/2,rightY+lineWidth/2,GL_POLYGON);
}

我们先定义了边界颜色绘制了边界,然后定义了图形的颜色来绘制填充的图形。其中 color 作为参数,可以传入不同的 RGB 值。 定义颜色 RGB 数组即可

1
2
3
4
5
6
int border[3]={0,0,0};
int grey[3]={195,195,195};
int yellow[3]={255,243,0};
int red[3]={237,28,36};
int darkGrey[3]={126,126,126};
int white[3]={255,255,255};

调用的时候我们只需要

1
glFillRect(-108,45,-81,0,darkGrey);

就可以绘制一个黑色边界的矩形了,填充色为暗灰色。 其他类似的方法实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//画填充的三角形,传值为逆时针,此法针对于机器人的正三角形
void glFillTri(int x1,int y1,int x2,int y2,int x3,int y3,int color[3]){
glColor3ub(border[0],border[1],border[2]);
glTri(x1,y1,x2,y2,x3,y3,GL_LINE_LOOP);
glColor3ub(color[0],color[1],color[2]);
glTri(x1+lineWidth/2,y1+lineWidth/2,x2-lineWidth/2,y2+lineWidth/2,x3,y3-lineWidth/2,GL_POLYGON);
}

//画填充的圆角矩形
void glFillRoundRec(int centerX,int centerY,int width,int height,float cirR,int color[3]){
glColor3ub(border[0],border[1],border[2]);
glRoundRec(centerX,centerY,width,height,cirR,GL_LINE_LOOP);
glColor3ub(color[0],color[1],color[2]);
glRoundRec(centerX,centerY,width-lineWidth/2,height-lineWidth/2,cirR,GL_POLYGON);
}

//画填充的圆形
void glFillCircle(double x, double y, double radius,int color[3]){
glColor3ub(border[0],border[1],border[2]);
glCircle(x,y,radius,GL_LINE_LOOP);
glColor3ub(color[0],color[1],color[2]);
glCircle(x,y,radius-lineWidth/2,GL_POLYGON);
}

恩,万事俱备只欠东风了,下面我们调用一下这几个函数直接进行绘制就好啦。 display 中的绘图调用如下

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
54
55
56
57
58
59
60
61
62
//函数用来画图
void display(void)
{
//GL_COLOR_BUFFER_BIT表示清除颜色
glClear(GL_COLOR_BUFFER_BIT);
glLineWidth(lineWidth);

//设置画线颜色
glColor3ub(0,0,0);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
//画圆角矩形,大脸
glFillRoundRec(0,113,128,74,10,grey);
//两个眼睛
glFillCircle(-30,111,10,white);
glFillCircle(30,111,10,white);
//黑色线
glColor3ub(0,0,0);
//两条天线
glLine(-35,150,-35,173);
glLine(35,150,35,173);
//圆弧,画嘴
glArc(0,133,11*PI/8,13*PI/8,45,GL_LINE_STRIP);
//画矩形,脖子
glFillRect(-25,76,25,60,grey);
//圆角矩形,两个耳朵
glFillRoundRec(81,115,20,34,5,darkGrey);
glFillRoundRec(-81,115,20,34,5,darkGrey);
//画圆角矩形,大肚子
glFillRoundRec(0,0,146,120,15,grey);
//画三角,肚子里的三角
glFillTri(-30,-15,30,-15,0,28,yellow);
//画圆,中间小圈
glFillCircle(0,0,10,red);
//画矩形,胳膊连接处
glFillRect(-81,43,-73,25,grey);
glFillRect(73,43,81,25,grey);
//画矩形,上臂
glFillRect(-108,45,-81,0,darkGrey);
glFillRect(81,45,108,0,darkGrey);
//画矩形,中臂
glFillRect(-101,0,-88,-4,grey);
glFillRect(88,0,101,-4,grey);
//画矩形,下臂
glFillRect(-108,-4,-81,-37,darkGrey);
glFillRect(81,-4,108,-37,darkGrey);
//画圆形,手掌
glFillCircle(-95,-47,10,grey);
glFillCircle(95,-47,10,grey);
//画腿连接处
glFillRect(-41,-62,-21,-66,grey);
glFillRect(21,-62,41,-66,grey);
//画圆角矩形,大长腿
glFillRoundRec(-32,-92,38,52,10,darkGrey);
glFillRoundRec(32,-92,38,52,10,darkGrey);
//画矩形,脚踝
glFillRect(-41,-117,-21,-125,grey);
glFillRect(21,-117,41,-125,grey);
//画矩形,大脚掌
glFillRect(-59,-125,-8,-137,darkGrey);
glFillRect(8,-125,59,-137,darkGrey);
glFlush();
}

恩,这样就大功告成啦,是不是比种子填充算法简单多了?

运行结果

QQ截图20150421000536 恩,那些小白点就忽略吧!!这叫不拘小节!!哈哈哈!!

总结

本节我们利用了类库的方法来实现了颜色填充,是不是很简单?

C/C++

综述

博主研究了一下午加一晚上,终于把种子填充算法实现出来并把机器人填充完毕,路途很艰辛,不过也学到了很多,在此和大家一起分享。

吐槽

与我不是同学的小伙伴,请自动忽略,我是来吐槽教材的。 在此不得不吐槽一下,不得不说教材实在太坑爹了。对于种子填充算法的后半部分,下一个种子点的寻找过程中,从 while(x<=xright)开始,我实在无法搞懂它里面的神逻辑,最初我认为它是对的,后来按照它的思路实现之后,填充基本上是错误的,比如圆角矩形下方的部分,它就无法正常填充。根本原因还是它的下一步种子点找错了,而博主依然在固执地 DeBug,看看是不是我哪里编码有问题。后来,干脆放弃了书上的逻辑了,自己改写了搜寻下一个种子点的算法,最后终于成功。 另外,教材上的这些伪代码写得也是太伪,算了,这不是重点,言归正传。

基本梳理

在博主的研究过程中,遇到了许许多多的小问题,在这里统一做一下总结,也希望大家少走弯路,吸取我的经验教训。

1.点的定义

在这里我们避免不了要使用点,一个点包括了 2 个元素,一个是横坐标一个是纵坐标,所以我们可以直接把它定义为一个结构体。

1
2
3
4
5
struct Point
{
int x;
int y;
};

这样的话,我们就可以直接声明一个 Point 类型的变量使用了,既方便又直观。

2.栈的使用

对于种子填充算法,肯定避免不了使用栈的,在这里博主分享一下一些使用心得。 栈的引入 C++代码中,可以直接用下面的代码来导入

1
2
#include <stack>
using namespace std;

注意,这里一定记得加上 using namespace std 这句话,否则会出现 stack 未定义的错误,哈哈哈,深有体会。 栈的定义 引入了栈之后,我们就可以直接来声明一个栈了

1
stack<Point> pixelStack;

其中,需要加一个尖括号,尖括号中声明了 Point 类型,这样我们就可以使用它了 取栈顶元素 C++中取栈顶元素是很坑的,有一个 top 方法,还有一个 pop 方法。 其中 top 方法是只取得栈顶的元素而不移除它,pop 方法是直接移除栈顶元素,没有返回值。 所以我们要想取出栈顶元素并移除的话,就要分别调用这两个方法

1
2
3
4
//获取最顶端的元素
Point tempPoint=pixelStack.top();
//删除最顶端的元素
pixelStack.pop();

是不是不友好?不友好的话,那就自己去定义一个新方法吧,我就先不这么干啦。 判断栈非空 判断当前的栈是否已经为空,只需要调用 empty 方法就可以了

1
2
3
while(!pixelStack.empty()){
//code
}

这里是一个 while 循环,如果栈为非空的话不断循环。 关于栈置空 教材中的种子填充算法中用到了栈置空,不过我感觉没有必要这么做,因为在方法最前面是新声明的栈变量,它一定是空的。不过如果非要置空的话,可以利用下面的代码

1
2
3
while(!pixelStack.empty()){
pixelStack.pop();
}

如果栈不为空,就一直取元素,就可以把它置空啦。

3.关于 glColor3b 和 glColor3ub

这的确也是坑得博主不浅,之前一直在用 glColor3b 这个方法来定义颜色,奇怪的是 glColor3b(255,0,0) 竟然不是红色,而是黑色!就是因为这个颜色问题,导致我在比对颜色的过程中走了很多弯路。在这里做一下说明 glColor3b()需要传入的是 byte 类型,它的数值范围是-128-127,也就是有符号数,我传入 255,由于越界了,255 这个数就相当于-128,难怪不变红啊。 glColor3ub()需要传入的是 unsigned byte 类型,范围是 0-255,无符号数,那么在这里我们传入 255,0,0 这三个数,就变成红色了。

4.取得某像素颜色

我想说的是,这也是个深坑啊,一下午的 Debug 全归它身上了。 获取某个像素的这个函数是

1
void glReadPixels(GLint x,GLint y,GLsizesi width,GLsizei height,GLenum format,GLenum type,GLvoid *pixel);

函数说明如下:

该函数总共有七个参数。前四个参数可以得到一个矩形,该矩形所包括的像素都会被读取出来。(第一、二个参数表示了矩形的左下角横、纵坐标,坐标以窗口最左下角为零,最右上角为最大值;第三、四个参数表示了矩形的宽度和高度) 第五个参数表示读取的内容,例如:GL_RGB 就会依次读取像素的红、绿、蓝三种数据,GL_RGBA 则会依次读取像素的红、绿、蓝、alpha 四种数据,GL_RED 则只读取像素的红色数据(类似的还有 GL_GREEN,GL_BLUE,以及 GL_ALPHA)。如果采用的不是 RGBA 颜色模式,而是采用颜色索引模式,则也可以使用 GL_COLOR_INDEX 来读取像素的颜色索引。目前仅需要知道这些,但实际上还可以读取其它内容,例如深度缓冲区的深度数据等。 第六个参数表示读取的内容保存到内存时所使用的格式,例如:GL_UNSIGNED_BYTE 会把各种数据保存为 GLubyte,GL_FLOAT 会把各种数据保存为 GLfloat 等。 第七个参数表示一个指针,像素数据被读取后,将被保存到这个指针所表示的地址。注意,需要保证该地址有足够的可以使用的空间,以容纳读取的像素数据。例如一幅大小为 256*256 的图象,如果读取其 RGB 数据,且每一数据被保存为 GLubyte。

好了,那么重点来了,这个方法的坐标基准点是在画布的左下角!!而我们绘图的基准点是在画布的正中心!!所以我在获取某个点的颜色的时候一直都是错误的结果,这样的话在使用的时候我们的 xy 坐标值就要加上画布宽高的一半才能正常获取到像素的颜色,希望大家一定注意!! 那么我们如何来使用呢?实例如下,首先定义 GLByte 的数组

1
GLubyte iPixel[3];

另外还有画布的宽度高度的一半变量

1
int halfWidth,halfHeight;

我们可以调用如下的方法来获取(x,y)这个点像素的值

1
glReadPixels(x+halfWidth,y+halfHeight,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);

在这里第五个参数我们定义了 GL_RGB,第六个参数我们定义了 GL_UNSIGNED_BYTE,最后是传入了数组的引用。 所以在调用这个方法之后,iPixel 数组里面的三个值就已经赋值为了该点的 RGB 值,可以拿来做下一步的判断。 比如我们边界可以定义为

1
GLubyte oldColor[3]={255,255,255};

在比较的时候就可以用下面的判别式

1
iPixel[0]!=borderColor[0]&&iPixel[1]!=borderColor[1]&&iPixel[2]!=borderColor[2]

这里我们是依次比较了三个 RGB 值是否与边界的 RGB 值相等,不过,有意思的是,识别颜色的这个方法,黑色的 RGB 值会识别成 1,1,1,而有时候在我调试的时候会识别为 0,1,1。我在想是不是系统计算误差问题,如果真是的话,因为这个小小的误差就影响了我们的判别条件岂不是亏大了?那么在这里我就定义了一个方法,允许一定的误差,这个误差姑且就称为 PS 里面的容差吧。

1
2
3
4
5
6
7
8
9
10
//传入两个颜色的RGB值,比较是否相同,容差为dis
bool sameColor(int r1,int g1,int b1,int r2,int g2,int b2){
//容差度
int dis = 10;
if(abs(r1-r2)<=dis&&abs(g1-g2)<=dis&&abs(b1-b2)<=dis){
return true;
}else{
return false;
}
}

那么我们的判定条件就改为了

1
!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])

这样系统误差便不会影响了。

5.下一个种子点的选取

教材上的种子点选取算法有点搞不懂,我按照上面的思路实现出来,在填充的时候出现了一系列问题。后来干脆放弃了教材中的方法,自己改写了一下。 思路大体上是这样的。

在填充完一行后,这一行最左边的像素点我们定义为(xLeft,y),最右边的像素我们定义为(xRight,y),扫描上一行找寻下一个种子点,这里 y 就要增加 1,如果(xRight,y+1)这个点不是边界不是已经填充的点,那么这个点就可以作为种子点压入堆栈。如果这个点是边界或者是已经填充的点,那么就继续往左搜索,如果找到既不是边界又未填充的点,那么这个点就是种子点,压入堆栈。如果一直往左找到 xLeft 还是没有找到的话,就不存在下一个种子点了。下一行扫描线也是同样的原理,y 要在这个基础上减去 2 即可。

恩,不知道大家有没有看懂,这是我自己想出来的方法,不敢保证完全正确,在此仅供参考。 如果大家真的可以按照教材中的方法实现成功的话,希望告诉我一下,感激不尽。

方法实现

恩,重要的地方都已经点明了,下面就直接附上我的种子填充算法吧!

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//种子填充算法
void zzFill(int startX,int startY,int r,int g,int b){
stack<Point> pixelStack;
//x,y是给定的种子像素点,rgb就是要填充的颜色的RGB值
Point point = {startX,startY};
pixelStack.push(point);
int saveX;
int xRight,xLeft;
int x,y;
//如果栈不为空
while(!pixelStack.empty()){
//获取最顶端的元素
Point tempPoint=pixelStack.top();
//删除最顶端的元素
pixelStack.pop();
saveX=tempPoint.x;
x=tempPoint.x;
y=tempPoint.y;
glReadPixels(x+halfWidth,y+halfHeight,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
//如果没有到达右边界,就填充
while(!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])){
glPoint(x,y,r,g,b);
x=x+1;
glReadPixels(x+halfWidth,y+halfHeight,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
}
xRight=x-1;
x=saveX-1;
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
//如果没有到达左边界,就填充
while(!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])){
glPoint(x,y,r,g,b);
x=x-1;
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
}
//保存左端点
xLeft=x+1;
//从右边的点开始
x=xRight;
//检查上端的扫描线
y=y+1;
while(x>=xLeft){
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
if(!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])&&!sameColor(iPixel[0],iPixel[1],iPixel[2],r,g,b)){
//如果上方的点不是边界点,直接压入
Point p={x,y};
pixelStack.push(p);
//压入之后停止循环
break;
}else{
x--;
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
}
}
//检查下端的扫描线
y=y-2;
//从右边的点开始
x=xRight;
while(x>=xLeft){
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
if(!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])&&!sameColor(iPixel[0],iPixel[1],iPixel[2],r,g,b)){
//如果上方的点不是边界点,直接压入
Point p={x,y};
//压入之后停止循环
pixelStack.push(p);
break;
}else{
x--;
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
}
}
}
}

以上便是我实现的种子填充算法,仅供参考 在这里我们用到了 glPoint 画点的方法,这是我们定义的,方法如下,为了便于调试,每画一个点刷新一下,这样我们就可以看到绘制的全部动态效果。

1
2
3
4
5
6
7
8
9
//画点
void glPoint(int x,int y,int r,int g,int b){
glColor3ub (r,g,b);
glPointSize(1);
glBegin(GL_POINTS);
glVertex2i(x,y);
glEnd();
glFlush();
}

以上便是画点的函数

方法使用

种子填充算法肯定要在我们绘制完机器人之后使用,任意选取某个四连通区域的点,传入 xy 坐标值还有要填充的颜色的 RGB 值,就可以成功实现填充。 在上一篇机器人的基础上,我们在画机器人的方法最后加入下面的代码,即可实现填充。

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
//灰色:195,195,195
//黄色:255,243,0
//红色:237,28,36
//深灰色:126,126,126
//脖子
zzFill(0,70,195,195,195);
//头
zzFill(-50,110,195,195,195);
zzFill(0,93,195,195,195);
//肚子
zzFill(-50,0,195,195,195);
//耳朵
zzFill(-80,115,126,126,126);
zzFill(80,115,126,126,126);
//肚子三角
zzFill(-20,-10,255,243,0);
//肚子红色圆
zzFill(0,0,237,28,36);
//zzFill(-50,0,128,255,33);
//大臂
zzFill(-90,30,126,126,126);
zzFill(90,30,126,126,126);
//小臂
zzFill(-90,-20,126,126,126);
zzFill(90,-20,126,126,126);
//手
zzFill(-75,40,195,195,195);
zzFill(75,40,195,195,195);
//手
zzFill(-95,-47,195,195,195);
zzFill(95,-47,195,195,195);
//大腿连接处
zzFill(-40,-64,195,195,195);
zzFill(40,-64,195,195,195);
//大腿
zzFill(-40,-100,126,126,126);
zzFill(40,-100,126,126,126);
//脚踝
zzFill(-40,-121,195,195,195);
zzFill(40,-121,195,195,195);
//脚掌
zzFill(-40,-130,126,126,126);
zzFill(40,-130,126,126,126);
system("pause");

注意,有个很奇怪地方是绘制完了之后机器人就不见了,所以在这里加入了 system(“pause”)方法来暂停一下就好啦。 其他的代码基本都是上一篇中的了,大家自行整理。

运行结果

运行结果截图如下 20150419015002 恩,就是这样!

总结

以上便是博主利用种子填充算法来实现的机器人的颜色填充,在此分享给大家,希望对大家有帮助! 如有问题和错误,欢迎大家给予我批评和指正,谢谢!

福利专区

综述

小伙伴们总要有一些秘密是不能让别人知道的,之前我们使用的设置隐藏文件夹然后在控制面板设置不显示隐藏文件夹的方式都弱爆了,下面我们来用一种更高级的办法来设置隐藏文件夹,感受一下。

设置隐藏

首先我们创建一个文件夹,比如名字叫 SECRET,如图所示 20150415114405 接下来我们打开命令行,输入如下命令

1
attrib +s +h e:/SECRET

20150415114811 输入命令之后,我们再查看一下 E 盘的内容,刷新一下,记得刷新!20150415120601 恩,那个文件夹已经不见了,有人说设置一下控制面板就显示出来了。 好,我们试试,控制面板设置显示隐藏的文件夹 20150415120426 恩,我们再看看,它依然是找不到的,就是这样! 20150415120601 那么我们怎么找到它?很简单,访问的时候只要在输入文件夹名称就会找到它了。 比如,我在地址栏中输入 E:/SECRET,就可以找到它了 20150415115101 下面我们来介绍一下这个命令的用法

attrib 命令用来显示或更改文件属性。

ATTRIB [+R | -R] [+A | -A ] [+S | -S] [+H | -H] [[drive:] [path] filename] [/S [/D]] + 设置属性。 - 清除属性。 R 只读文件属性。 A 存档文件属性。 S 系统文件属性。 H 隐藏文件属性。 [drive:][path][filename] 指定要处理的文件属性。 /S 处理当前文件夹及其子文件夹中的匹配文件。 /D 也处理文件夹。

比如

1
attrib +a +s +r +h e:/SECRET

这句命令就是设置 E 盘的 SECRET 文件夹为存档文件、系统文件、只读文件、隐藏文件。

在这里我们只要设置为隐藏文件盒系统文件属性就可以完成文件夹的隐藏,是不是很酷炫?

取消隐藏

了解了上面的命令,取消隐藏就很简单啦,我们只需要输入下面的命令

1
attrib -h -s e:/SECRET

再刷新一下目录,SECRET 目录就又出现了。

20150415115831

恩,会了吧,小伙伴们还不赶紧试试。不用谢我,请叫我雷锋!

C/C++

综述

在上一篇文章我们介绍了利用类库来完成一个机器人绘制的过程,这里我们一起来看一下怎样直接利用直线和圆弧生成算法来进行图形的绘制。 P.S. 本篇文章针对《计算机图形学》张彩明 版来探讨学习。关于书中的详细算法不会再赘述。 P.P.S. 本篇文章算法扩展思路及代码实现为博主原创内容,如存在纰漏和错误,希望大家指正。

直线生成算法

1.DDA 算法

DDA 算法是最基本的一种直线生成算法了,代码实现简单,不过缺点是计算量比较大,画一个点要两次加法,两次取整运算。另外,DDA 算法还包括了除法运算。不仅算法复杂,而且硬件实现上有一定的难度。优点就是程序简单易懂,在这里实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//画直线的DDA算法
void dda(int x1,int y1,int x2,int y2){
int k,i;
float x,y,dx,dy;
//使k等于横纵坐标差值的较大者
k = abs(x2-x1);
if(abs(y2-y1)>k)
k = abs(y2-y1);
//直线被划分为每一小段的长度
dx = float(x2-x1)/k;
dy = float(y2-y1)/k;
x = float(x1);
y = float(y1);
for(i = 0;i<k;i++){
//此处先加0.5再取整的作用是四舍五入
gl_Point((int)(x+0.5),(int)(y+0.5));
//x和y分别增加相应的单位
x = x+dx;
y = y+dy;
}
}

解释一下这里的 gl_Point 方法,这个方法并不是直接调用类库的方法,而是我们自己来实现的画点的方法。

1
2
3
4
5
6
//画点
void gl_Point(int x,int y){
glBegin(GL_POINTS);
glVertex2i(x,y);
glEnd();
}

用来画点的话,我们必须要在 glBegin 方法传入 GL_POINTS 参数,然后利用类库中画点的方法来绘制点。

2.正负法

在教材中只讨论了斜率在 0-1 之间的情况,代码的实现也是仅仅只有 0-1 一种情况,对于斜率大于 1,斜率在-1 和 0 之间以及斜率小于-1 的情况没有加以讨论。 如果我们直接拿来教材中的代码来用,我们会发现只能绘制出 0-1 斜率的直线,对于其他的情况,均绘制错误。 所以我们需要分四种情况来讨论,直线方程 F(x,y)=ax+by+c=0,其中 a=ys-ye,b=xe-xs 设直线的斜率为 k,讨论分类如下 (1)k∈[0,1) 此时有 a<0,b>0 d=F(M)=F(x+1,y+0.5)=a(x+1)+b(y+0.5)+c 当 d>=0 时,Q 在 M 点下方,取右下方的点,d1=F(x+2,y+0.5)=a(x+2)+b(y+0.5)+c=d+a 当 d<0 时,Q 在 M 点上方,取右上方的点,d2=F(x+2,y+1.5)=a(x+2)+b(y+1.5)+c=d+a+b 此时 d 的初始值 d0=F(xs+1,ys+0.5)=a+0.5b **(2)k∈[1,+∞]** 此时有 a<0,b>0 d=F(M)=F(x+0.5,y+1)=a(x+0.5)+b(y+1)+c 当 d>=0 时,Q 在 M 点右侧,取右上方的点,d1=F(x+1.5,y+2)=a(x+1.5)+b(y+2)+c=a+b+d 当 d<0 时,Q 在 M 点左侧,取左上方的点,d2=F(x+0.5,y+2)=b+d 此时 d 的初始值 d0=F(xs+0.5,ys+1)=0.5a+b **(3)k∈[-1,0)** 此时有 a>0,b>0 d=F(M)=F(x+1,y-0.5)=a(x+1)+b(y-0.5)+c 当 d>=0 时,Q 在 M 点下方,取右下方的点,d1=F(x+2,y-1.5)=a(x+2)+b(y-1.5)+c=a-b+d 当 d<0 时,Q 在 M 点上方,取右上方的点,d2=F(x+2,y-0.5)=a(x+2)+b(y-0.5)+c=a+d 此时 d 的初始值 d0=F(xs+1,ys-0.5)=a-0.5b **(4)k∈[-∞,-1)** 此时有 a>0,b>0 d=F(M)=F(x+0.5,y-1)=a(x+0.5)+b(y-1)+c 当 d>=0 时,Q 在 M 点左方,取左下方的点,d1=F(x+0.5,y-2)=a(x+0.5)+b(y-2)+c=d-b 当 d<0 时,Q 在 M 点右方,取右下方的点,d2=F(x+1.5,y-2)=a(x+1.5)+b(y-2)+c=a-b+d 此时 d 的初始值 d0=F(xs+0.5,ys-1)=0.5a-b 注意:上面的 a 和 b 的符号,是在默认起点在终点的左侧来看待的 所以,如果我们传入参数时,第二个点在第一个点的左侧时,我们可能就不会得到正确的结果。所以当我们发现第二个点不在第一个点右侧时,就需要把二者的横纵坐标交换。 代码实现如下,此实现仅供参考,未经优化。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//画直线的正负法
void midPointLine(int xs,int ys,int xe,int ye){
if(xs>xe){
swap(xs,xe);
swap(ys,ye);
}
float a,b,dt1,dt2,d,x,y;
a=ys-ye;
b=xe-xs;
float k =(float)(ye-ys)/(xe-xs);
if(k>=0&&k<1){
d=2*a+b;
dt1=2*a;
dt2=2*(a+b);
}else if(k>=1){
d=a+2*b;
dt1=2*(a+b);
dt2=2*b;
}else if(k<0&&k>=-1){
d=2*a-b;
dt1=2*(a-b);
dt2=2*a;
}else if(k<-1){
d=a-2*b;
dt1=-2*b;
dt2=2*(a-b);
}
x=xs;y=ys;
gl_Point(x,y);
if(k>=0&&k<1){
while(x<xe){
if(d<0){
x++;y++;d+=dt2;
}else{
x++;d+=dt1;
}
gl_Point(x,y);
}
}else if(k>=1){
while(y<ye){
if(d<0){
y++;d+=dt2;
}else{
y++;x++;d+=dt1;
}
gl_Point(x,y);
}
}else if(k<0&&k>=-1){
while(x<xe){
if(d<0){
x++;d+=dt2;
}else{
x++;y--;d+=dt1;
}
gl_Point(x,y);
}
}else if(k<-1){
while(y>ye){
if(d<0){
y--;x++;d+=dt2;
}else{
y--;d+=dt1;
}
gl_Point(x,y);
}
}
}

在一开始我们用到了 swap 方法,是用来交换两个数字的,实现如下

1
2
3
4
5
6
//交换两个数字
void swap(int &x1,int &x2){
int temp = x2;
x2=x1;
x1=temp;
}

以上便是正负法的实现,代码仅供参考。

3.Bresenham 算法

在教材中,同样是只针对斜率在 0-1 之间讨论。对于教材中的程序,我们也只能绘制斜率为 0-1 的直线,所以我们需要对另外三种情况进行扩充。 分类讨论如下 (1)k∈[0,1) 即教材中的讲解方法 (2)k∈[1,+∞] 需要把 x 和 y 互换即可 (3)k∈[-1,0) x 不变,y 换为-y (4)k∈[-∞,-1) x 换为-y,y 换为 x 程序实现如下

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
54
55
56
57
//画直线的Bresenham方法
void bresen(int x1,int y1,int x2,int y2){
if(x2<x1){
swap(x1,x2);
swap(y1,y2);
}
float dx=x2-x1;
float dy=y2-y1;
float k=dy/dx;
float m,e;int i;
float x=x1,y=y1;
if(k>=0&&k<1){
//斜率为0到1
m=dy/dx;
e=m-0.5;
for(i=0;i<dx;i++){
gl_Point(x,y);
if(e>=0){
y+=1;e-=1;
}
x+=1;e+=m;
}
}else if(k>=1){
//x换为y,y换为x
m=dx/dy;
e=m-0.5;
for(i=0;i<dy;i++){
gl_Point(x,y);
if(e>=0){
x+=1;e-=1;
}
y+=1;e+=m;
}
}else if(k<0&&k>=-1){
//x不变,y换为-y
m=-dy/dx;
e=m+0.5;
for(i=0;i<dx;i++){
gl_Point(x,y);
if(e<=0){
y-=1;e+=1;
}
x+=1;e-=m;
}
}else{
//将x换为-y,y换为x
m=-dx/dy;
e=m+0.5;
for(i=0;i<-dy;i++){
gl_Point(x,y);
if(e<=0){
x+=1;e+=1;
}
y-=1;e-=m;
}
}
}

以上便是 Bresenham 算法,经测试通过。

圆弧生成算法

1.正负法

教材中只讨论了圆弧在第一象限的情况,不过有趣的是,圆是具有对称性的,在绘制圆形时,我们如果把 x 换为-x,就可以绘制第二象限的图形,把 y 换为-y,就可以绘制第四象限的图形,代码也不需要改动很多。只需要在 gl_Point 上面下功夫即可。 另外,教材中圆弧生成算法中没有指定圆的中心点的坐标,我们可以把它当做参数来传递进来,然后传入 gl_Point 绘图函数即可,相当方便。 注:此方法不需要再繁琐地分类讨论。 在这里给出博主写出的两种方法,一种是如上所介绍的思路,利用对称性,另一种是分类讨论的思想。 (1)利用对称性实现

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
//正负法画圆
void pnarcArc(int radius,int centerX,int centerY,int area){
int x,y,f;
x=0;y=0+radius;f=0;
while(y>0){
switch(area){
case 1:
gl_Point(x+centerX,y+centerY);
break;
case 2:
gl_Point(-x+centerX,y+centerY);
break;
case 3:
gl_Point(-x+centerX,-y+centerY);
break;
case 4:
gl_Point(x+centerX,-y+centerY);
break;
}
if(f>0){
f=f-2*y+1;
y=y-1;
}else{
f=f+2*x+1;
x=x+1;
}
}
if(y==centerY){
gl_Point(x,y);
}
}

(2)分类讨论思想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//正负法画圆3
void pnarcArc(int radius,int centerX,int centerY,int area){
int x,y,f;
int tag[4]={1,1,-1,-1};
int tagX[4]={1,-1,-1,1};
int tagY[4]={-1,-1,1,1};
x=0;y=tag[area-1]*radius;f=0;
while(tag[area-1]*y>0){
gl_Point(x+centerX,y+centerY);
if(f>0){
f=f+tagY[area-1]*2*y+1;
y=y+tagY[area-1];
}else{
f=f+tagX[area-1]*2*x+1;
x=x+tagX[area-1];
}
}
if(y==centerY){
gl_Point(x,y);
}
}

在上面的代码中,我们传入了 centerX 和 centerY 以及 area 参数。其中 centerX 和 centerY 是圆弧中心点的坐标,area 是所在的象限,传入的参数需要是 1,2,3,4 中的一个数字,如果传入其他数字则不会绘制出任何图形。 注:此方法只能一次性绘制一个四分之一圆弧,局限性比较大,如果要融入弧度,改动量比较大。

2.Bresenham 算法

和上面方法类似,我们的实现同样非常简单,即使教材中只讨论了八分之一圆,我们可以利用对称的思想来实现画圆。另外我们添加了圆弧中心点坐标已经所在的区块。 从(π/4,π/2)这个区块开始,编号为 1,角度为 45°,顺时针旋转(0,π/4)的编号为 2,以此类推,参数变量为 area 代码实现如下

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
//Bresenham画圆算法
void bresenhamArc(int R,int centerX,int centerY,int area){
int x,y,d;
x=0;y=R;d=3-2*R;
while(x<y){
switch(area){
case 1:
gl_Point(x+centerX,y+centerY);
break;
case 2:
gl_Point(y+centerX,x+centerY);
break;
case 3:
gl_Point(y+centerX,-x+centerY);
break;
case 4:
gl_Point(x+centerX,-y+centerY);
break;
case 5:
gl_Point(-x+centerX,-y+centerY);
break;
case 6:
gl_Point(-y+centerX,-x+centerY);
break;
case 7:
gl_Point(-y+centerX,x+centerY);
break;
case 8:
gl_Point(-x+centerX,y+centerY);
break;
}
if(d<0){
d=d+4*x+6;
}else{
d=d+4*(x-y)+10;
y=y-1;
}
x=x+1;
}
if(x==y){
gl_Point(x,y);
}
}

此段代码亲测可用,仅供参考。

终极目标

上一节我们实现了用类库的方法和 sin,cos 方法来定位坐标绘制机器人,在这一节我们就利用上述的直线和圆弧生成算法,对上一篇中的机器人进行绘制。 在这里只贴出最核心的部分,那就是绘画的函数了,只是简单地传入坐标点然后调用刚才实现的一些方法,比较繁琐,但是比较简单,核心代码如下

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//myDisplay函数用来画图
void myDisplay(void){
//清除。GL_COLOR_BUFFER_BIT表示清除颜色,glClear函数还可以清除其它的东西
glClear(GL_COLOR_BUFFER_BIT);
//设置黑色颜色
glColor3f (0.0f, 0.0f, 0.0f);
//中心的圆圈,四个象限,半径为10
pnarcArc(10,0,0,1);
pnarcArc(10,0,0,2);
pnarcArc(10,0,0,3);
pnarcArc(10,0,0,4);
//肚子中三角形,利用了DDA绘制
dda(-30,-15,30,-15);
dda(-30,-15,0,28);
dda(0,28,30,-15);
//肚子,四条直线
midPointLine(-60,60,60,60);
midPointLine(-60,-60,60,-60);
midPointLine(-74,46,-74,-46);
midPointLine(74,46,74,-46);
//肚子,四个半圆
pnarcArc(14,60,46,1);
pnarcArc(14,-60,46,2);
pnarcArc(14,-60,-46,3);
pnarcArc(14,60,-46,4);
//脖子,两条直线
bresen(-25,76,-25,60);
bresen(26,76,25,60);
//脸,四条直线
midPointLine(-54,150,54,150);
midPointLine(-54,76,54,76);
midPointLine(-64,140,-64,86);
midPointLine(64,140,64,86);
//脸,四个圆弧
pnarcArc(10,54,140,1);
pnarcArc(10,-54,140,2);
pnarcArc(10,-54,86,3);
pnarcArc(10,54,86,4);
//眼睛,两个正圆
pnarcArc(10,-30,111,1);
pnarcArc(10,-30,111,2);
pnarcArc(10,-30,111,3);
pnarcArc(10,-30,111,4);
pnarcArc(10,30,111,1);
pnarcArc(10,30,111,2);
pnarcArc(10,30,111,3);
pnarcArc(10,30,111,4);
//嘴巴,两个八分之一圆
bresenhamArc(20,0,111,4);
bresenhamArc(20,0,111,5);
//天线
dda(-35,150,-35,173);
dda(35,150,35,173);
//右耳朵,四条直线
bresen(88,98,88,131);
bresen(67,98,67,131);
bresen(70,95,85,95);
bresen(70,134,85,134);
//右耳朵,四个圆弧
pnarcArc(3,85,131,1);
pnarcArc(3,70,131,2);
pnarcArc(3,70,98,3);
pnarcArc(3,85,98,4);
//左耳朵,四条直线
bresen(-88,98,-88,131);
bresen(-67,98,-67,131);
bresen(-70,95,-85,95);
bresen(-70,134,-85,134);
//左耳朵,四个圆弧
pnarcArc(3,-70,131,1);
pnarcArc(3,-85,131,2);
pnarcArc(3,-85,98,3);
pnarcArc(3,-70,98,4);
//左胳膊衔接处
bresen(-73,25,-80,25);
bresen(-73,43,-80,43);
//右胳膊衔接处
bresen(73,25,80,25);
bresen(73,43,80,43);
//左大臂
dda(-108,45,-108,0);
dda(-81,45,-81,0);
dda(-108,45,-81,45);
dda(-108,0,-81,0);
//右大臂
dda(108,45,108,0);
dda(81,45,81,0);
dda(108,45,81,45);
dda(108,0,81,0);
//左中臂
bresen(-101,0,-101,-4);
bresen(-88,0,-88,-4);
//右中臂
bresen(101,0,101,-4);
bresen(88,0,88,-4);
//左小臂
dda(-108,-4,-108,-37);
dda(-81,-4,-81,-37);
dda(-108,-4,-81,-4);
dda(-108,-37,-81,-37);
//右小臂
dda(108,-4,108,-37);
dda(81,-4,81,-37);
dda(108,-4,81,-4);
dda(108,-37,81,-37);
//左手
pnarcArc(10,-95,-47,1);
pnarcArc(10,-95,-47,2);
pnarcArc(10,-95,-47,3);
pnarcArc(10,-95,-47,4);
//右手
pnarcArc(10,95,-47,1);
pnarcArc(10,95,-47,2);
pnarcArc(10,95,-47,3);
pnarcArc(10,95,-47,4);
//左腿衔接处
dda(-43,-62,-43,-69);
dda(-25,-62,-25,-69);
//右腿衔接处
dda(43,-62,43,-69);
dda(25,-62,25,-69);
//左大腿,四条直线
bresen(-47,-69,-21,-69);
bresen(-47,-117,-21,-117);
bresen(-51,-70,-51,-113);
bresen(-17,-70,-17,-113);
//左大腿,四条圆弧
pnarcArc(4,-21,-73,1);
pnarcArc(4,-47,-73,2);
pnarcArc(4,-47,-113,3);
pnarcArc(4,-21,-113,4);
//右大腿,四条直线
bresen(47,-69,21,-69);
bresen(47,-117,21,-117);
bresen(51,-70,51,-113);
bresen(17,-70,17,-113);
//右大腿,四条圆弧
pnarcArc(4,47,-73,1);
pnarcArc(4,21,-73,2);
pnarcArc(4,21,-113,3);
pnarcArc(4,47,-113,4);
//左脚踝
dda(-43,-118,-43,-125);
dda(-25,-118,-25,-125);
//右腿衔接处
dda(43,-118,43,-125);
dda(25,-118,25,-125);
//左脚
bresen(-59,-125,-8,-125);
bresen(-59,-137,-8,-137);
bresen(-59,-125,-59,-137);
bresen(-8,-125,-8,-137);
//右脚
bresen(59,-125,8,-125);
bresen(59,-137,8,-137);
bresen(59,-125,59,-137);
bresen(8,-125,8,-137);
//刷新机器人
glFlush();
}

其他的部分不再赘述,都十分基础。

运行结果

直接贴图如下 QQ截图20150413005949 嘿嘿,我们直接用生成算法绘制的图形是不是更好看一些呢?

总结

本节介绍了各种直线生成算法和圆弧生成算法,以及利用该算法重新绘制机器人,希望对大家有帮助!

C/C++

综述

计算机图形学教材中有多种绘图方法,如直线的 DDA 算法、正负法、Bresenham 算法和画圆弧的正负法和 Bresenham 算法。 同样,OpenGL 类库也为我们提供了多种绘图方法,比如 glVertex2d,在这里我们用类库的方法来实现一个机器人的绘制。DDA 等算法实现之后我们再替换类库的 glVertex2d 方法。

绘制要求

利用 glVertex2d 和 glVertex2f 在二维平面上绘制如下的机器人。所以我们现在不需要三维的绘图方法,仅在平面绘制即可。 20150410115231

问题分析

经过观察我们发现,图中包含了圆角矩形,矩形,直线,圆形,弧形,三角形,而对于 glVertex2d 的方法,只是定位好坐标点,然后利用坐标点连线或者形成封闭图形来绘制。所以,对于圆角矩形,弧形等,我们可以利用三角函数来求取坐标,并连线即可。

1.圆角矩形的解决方案

对于圆角矩形,顾名思义每个角是由一个四分之一圆弧组成的。我们定义这个圆弧的半径为 cirR,整个圆角矩形的宽度为 width,高度为 height,那么抛出四个角的圆弧,就会在圆角矩形内部形成一个小的矩形,我们定义它的宽高分别为 w,h。另外,圆角矩形的中心点为(centerX,centerY),如下图所示 (博主原创图,盗图必究) 在 OpenGL 中,可以定义一个绘图模式

1
glBegin(GL_LINE_LOOP);

可以绘制封闭曲线,也就是说,我们所有调用的 glVertex2f 函数绘制的点均可以自动连接成一个封闭曲线。 所以,我们要做的就是找出圆角的坐标,利用 C++ 中的三角函数来计算右上角小圆所在的路径,我们设角度为 X,所以圆弧的 x 方向延伸长度为 cirRcosX,在 y 方向延伸长度为 cirRsinX 对于右上角的四分之一圆弧,它所在的路径 x 坐标便是 centerX+w/2+cirRcosX = centerX+width/2-cirR+cirRcosX,同理,y 路径坐标便是 centerY+h/2+cirRsinX = centerY+height/2-cirR+cirRsinX 当然对于其他的角,是加 w/2 还是减 w/2 就要看它所在的象限了。 另外对于精确度的问题,我们可以定义一个 divide 变量,将四分之一圆弧分成若干个点来绘制,当然分的份数越多,越精细,分的份数的值就是 divide,所以每次增加的弧度便是 PI/(2*divide) 方法最终实现如下,我们传入矩形的宽高,矩形中心的坐标,圆角半径即可进行绘制。

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
//画圆角矩形,传入矩形中心点坐标,矩形宽高,角半径
void glRoundRec(int centerX,int centerY,int width,int height,float cirR){
//二分之PI,一个象限的角度
float PI_HALF = PI/2;
//划分程度,值越大画得越精细
float divide=20.0;
//圆角矩形的坐标
float tx,ty;
//画封闭曲线
glBegin(GL_LINE_LOOP);
//四个象限不同的操作符
int opX[4]={1,-1,-1,1};
int opY[4]={1,1,-1,-1};
//用来计数,从第一象限到第四象限
float x=0;
//x自增时加的值
float part=1/divide;
//计算内矩形宽高一半的数值
int w=width/2-cirR;
int h=height/2-cirR;
//循环画线
for(x=0;x<4;x+=part){
//求出弧度
float rad = PI_HALF*x;
//计算坐标值
tx=cirR*cos(rad)+opX[(int)x]*w+centerX;
ty=cirR*sin(rad)+opY[(int)x]*h+centerY;
//传入坐标画线
glVertex2f(tx,ty);
}
//结束画线
glEnd();
}

例如我们可以调用

1
glRoundRec(0,0,146,120,15);

可以绘制如下的圆角矩形

2.圆弧的解决方案

对于圆弧的画法,我们也可以利用三角函数来解决,所以我们需要知道的参数就有半径,起始角度,还有圆弧的中心点。 代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径
void glArc(double x,double y,double start_angle,double end_angle,double radius)
{
//开始绘制曲线
glBegin(GL_LINE_STRIP);
//每次画增加的弧度
double delta_angle=PI/180;
//画圆弧
for (double i=start_angle;i<=end_angle;i+=delta_angle)
{
//绝对定位加三角函数值
double vx=x+radius * cos(i);
double vy=y+radius*sin(i);
glVertex2d(vx,vy);
}
//结束绘画
glEnd();
}

至于画圆,我们只需要画一个弧度为 0 至 2PI 的圆弧即可。

1
2
3
4
5
6
//画圆
void glCircle(double x, double y, double radius)
{
//画全圆
glArc(x,y,0,2*PI,radius);
}

3.其他图形的解决方案

对于直线,三角形,矩形等等,就没有那么复杂了,只需要绘制几个端点即可完成绘制。在此不再赘述。

4.坐标的解决方案

要画图,最重要的便是坐标点,在此博主对图上的某些坐标进行了测量,假设总宽度为 300,对应的坐标值标注如下图所示,当然比例不一样的话,坐标会有成比例的变化,在此仅作参考。 robot 在图中,某些点的坐标已做好标注,仅供参考。

完整程序

完整的画机器人的程序实现如下

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#include <glut.h>
#include <math.h>
#define PI 3.1415926

//画矩形,传入的是左下角XY坐标和右上角XY坐标
void glRect(int leftX,int leftY,int rightX,int rightY){
//画封闭曲线
glBegin(GL_LINE_LOOP);
//左下角
glVertex2d(leftX,leftY);
//右下角
glVertex2d(rightX,leftY);
//右上角
glVertex2d(rightX,rightY);
//左上角
glVertex2d(leftX,rightY);
//结束画线
glEnd();
}

//画圆角矩形,传入矩形宽高,角半径,矩形中心点坐标
void glRoundRec(int centerX,int centerY,int width,int height,float cirR){
//二分之PI,一个象限的角度
float PI_HALF = PI/2;
//划分程度,值越大画得越精细
float divide=20.0;
//圆角矩形的坐标
float tx,ty;
//画封闭曲线
glBegin(GL_LINE_LOOP);
//四个象限不同的操作符
int opX[4]={1,-1,-1,1};
int opY[4]={1,1,-1,-1};
//用来计数,从第一象限到第四象限
float x=0;
//x自增时加的值
float part=1/divide;
//计算内矩形宽高一半的数值
int w=width/2-cirR;
int h=height/2-cirR;
//循环画线
for(x=0;x<4;x+=part){
//求出弧度
float rad = PI_HALF*x;
//计算坐标值
tx=cirR*cos(rad)+opX[(int)x]*w+centerX;
ty=cirR*sin(rad)+opY[(int)x]*h+centerY;
//传入坐标画线
glVertex2f(tx,ty);
}
//结束画线
glEnd();
}

//画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径
void glArc(double x,double y,double start_angle,double end_angle,double radius)
{
//开始绘制曲线
glBegin(GL_LINE_STRIP);
//每次画增加的弧度
double delta_angle=PI/180;
//画圆弧
for (double i=start_angle;i<=end_angle;i+=delta_angle)
{
//绝对定位加三角函数值
double vx=x+radius * cos(i);
double vy=y+radius*sin(i);
glVertex2d(vx,vy);
}
//结束绘画
glEnd();
}


//画圆
void glCircle(double x, double y, double radius)
{
//画全圆
glArc(x,y,0,2*PI,radius);
}

//画三角形,传入三个点的坐标
void glTri(int x1,int y1,int x2,int y2,int x3,int y3){
//画封闭线
glBegin(GL_LINE_LOOP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//三点
glVertex2d(x3,y3);
//结束画线
glEnd();
}

//画线,传入两点坐标
void glLine(int x1,int y1,int x2,int y2){
//画封闭线
glBegin(GL_LINE_STRIP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//结束画线
glEnd();
}

//函数用来画图
void display(void)
{
//GL_COLOR_BUFFER_BIT表示清除颜色
glClear(GL_COLOR_BUFFER_BIT);
//设置画线颜色
glColor3f(0.5,0.5,0.5);
//画点大小
glPointSize(2);
//画圆角矩形,大肚子
glRoundRec(0,0,146,120,15);
//画圆,中间小圈
glCircle(0,0,10);
//画矩形,脖子
glRect(-25,60,25,76);
//画圆角矩形,大脸
glRoundRec(0,113,128,74,10);
//两个眼睛
glCircle(-30,111,10);
glCircle(30,111,10);
//两条天线
glLine(-35,150,-35,173);
glLine(35,150,35,173);
//圆角矩形,两个耳朵
glRoundRec(81,115,20,34,5);
glRoundRec(-81,115,20,34,5);
//圆弧,画嘴
glArc(0,133,11*PI/8,13*PI/8,45);
//画三角,肚子里的三角
glTri(-30,-15,30,-15,0,28);
//画矩形,胳膊连接处
glRect(-81,43,-73,25);
glRect(81,43,73,25);
//画矩形,上臂
glRect(-108,45,-81,0);
glRect(108,45,81,0);
//画矩形,中臂
glRect(-101,0,-88,-4);
glRect(101,0,88,-4);
//画矩形,下臂
glRect(-108,-4,-81,-37);
glRect(108,-4,81,-37);
//画圆形,手掌
glCircle(-95,-47,10);
glCircle(95,-47,10);
//画腿连接处
glRect(-41,-62,-21,-66);
glRect(41,-62,21,-66);
//画圆角矩形,大长腿
glRoundRec(-32,-92,38,52,10);
glRoundRec(32,-92,38,52,10);
//画矩形,脚踝
glRect(-41,-125,-21,-117);
glRect(41,-125,21,-117);
//画矩形,大脚掌
glRect(-59,-125,-8,-137);
glRect(59,-125,8,-137);

//保证前面的OpenGL命令立即执行,而不是让它们在缓冲区中等待
glFlush();
}


//窗口大小变化时调用的函数
void ChangeSize(GLsizei w,GLsizei h)
{
//避免高度为0
if(h==0) {
h=1;
}
//定义视口大小,宽高一致
glViewport(0,0,w,h);
int half = 200;
//重置坐标系统,使投影变换复位
glMatrixMode(GL_PROJECTION);
//将当前的用户坐标系的原点移到了屏幕中心
glLoadIdentity();
//定义正交视域体
if(w<h) {
//如果高度大于宽度,则将高度视角扩大,图形显示居中
glOrtho(-half,half,-half*h/w,half*h/w,-half,half);
} else {
//如果宽度大于高度,则将宽度视角扩大,图形显示居中
glOrtho(-half*w/h,half*w/h,-half,half,-half,half);
}

}

//程序入口
int main(int argc, char *argv[]){
//对GLUT进行初始化,并处理所有的命令行参数
glutInit(&argc, argv);
//指定RGB颜色模式和单缓冲窗口
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
//定义窗口的位置
glutInitWindowPosition(100, 100);
//定义窗口的大小
glutInitWindowSize(400, 400);
//创建窗口,同时为之命名
glutCreateWindow("OpenGL");
//设置窗口清除颜色为白色
glClearColor(1.0f,1.0f,1.0f,1.0f);
//参数为一个函数,绘图时这个函数就会被调用
glutDisplayFunc(&display);
//参数为一个函数,当窗口大小改变时会被调用
glutReshapeFunc(ChangeSize);
//该函数让GLUT框架开始运行,所有设置的回调函数开始工作,直到用户终止程序为止
glutMainLoop();
//程序返回
return 0;
}

运行效果如下图所示 20150410152854

综述

当然,这里直接调用了类库中的画线方法,而且用的是三角函数定位坐标。对于 DDA 算法等实现还没有进行替换,等实现 DDA 算法之后,再将绘制直线,圆弧等等的算法替换掉。同样可以实现机器人的绘制。 希望对大家有帮助!

JavaScript

综述

我们都见过淘宝上的宝贝,把鼠标放上去,会有局部放大的功能,现在我们可以利用一个叫jQZoom的插件,来实现图片的局部放大,让我们来感受一下

在线演示

我们首先来在线演示一下效果,然后我们说一下是怎样的实现,点开链接进行预览吧 在线预览

插件文件

其中包含了两个JS文件,一个是jQuery库,另一个就是jqzoom插件文件。另外还有一个css文件,主要作用是给图片放缩规定样式

HTML

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
<!DOCTYPE html>
<html>
<head>
<title>JQZoom放大镜</title>
<meta charset="utf-8">
<script src="js/jquery.min.js"></script>
<script src="js/jquery.jqzoom.js"></script>
<link rel="stylesheet" type="text/css" href="css/jquery.jqzoom.css" />
<style type="text/css">
body{font-size:13px}
span{color:Red;font-size:12px}
.divFrame{width:260px;border:solid 1px #666}
.divFrame .divTitle{padding:5px;background-color:#eee;font-weight:bold}
.divFrame .divContent{padding:8px;line-height:1.6em}
.divFrame .divContent img{border:1px solid #ccc}
</style>
<script type="text/javascript">
$(function() {
$("#jqzoom").jqzoom( //绑定图片放大插件jqzoom
{
zoomWidth: 230,
zoomHeight: 230,
position: 'right'
}
);
});
</script>
</head>
<body>
<div class="divFrame">
<div class="divTitle">
图片放大镜
</div>
<div class="divContent">
<a href="images/bag.jpg" id="jqzoom" title="我的背包">
<img src="images/bagsmall.jpg">
</a>
</div>
</div>
</body>
</html>

Demo的代码如上所示,下面我们来分析一下代码调用

代码分析

1
2
3
<a href="images/bag.jpg" id="jqzoom" title="我的背包">
<img src="images/bagsmall.jpg">
</a>

最主要的部分如上所示,是一个超链接包含了一张图片。超链接的href是图片的大图,里面的img的src是小图。 利用插件时,只需要取到超链接这个元素,然后调用jqzoom方法就可以了

1
2
3
4
5
6
7
$("#jqzoom").jqzoom( //绑定图片放大插件jqzoom
{
zoomWidth: 230,
zoomHeight: 230,
position: 'right'
}
);

用法很简单,只需要传入一些参数就可以了。

参数详解

下面是所有的参数详解

  • zoomType,默认值:’standard’,另一个值是’reverse’,是否将原图用半透明图层遮盖。
  • zoomWidth,默认值:200,放大窗口的宽度。
  • zoomHeight,默认值:200,放大窗口的高度。
  • xOffset,默认值:10,放大窗口相对于原图的x轴偏移值,可以为负。
  • yOffset,默认值:0,放大窗口相对于原图的y轴偏移值,可以为负。
  • position,默认值:’right’,放大窗口的位置,值还可以是:’right’ ,’left’ ,’top’ ,’bottom’。
  • lens,默认值:true,若为false,则不在原图上显示镜头。
  • imageOpacity,默认值:0.2,当zoomType的值为’reverse’时,这个参数用于指定遮罩的透明度。
  • title,默认值:true,在放大窗口中显示标题,值可以为a标记的title值,若无,则为原图的title值。
  • showEffect,默认值:’show’,显示放大窗口时的效果,值可以为: ‘show’ ,’fadein’。
  • hideEffect,默认值:’hide’,隐藏放大窗口时的效果: ‘hide’ ,’fadeout’。
  • fadeinSpeed,默认值:’fast’,放大窗口的渐显速度(选项: ‘fast’,’slow’,’medium’)。
  • fadeoutSpeed,默认值:’slow’,放大窗口的渐隐速度(选项: ‘fast’,’slow’,’medium’)。
  • showPreload,默认值:true,是否显示加载提示Loading zoom(选项: ‘true’,’false’)。
  • preloadText,默认值:’Loading zoom’,自定义加载提示文本。
  • preloadPosition,默认值:’center’,加载提示的位置,值也可以为’bycss’,以通过css指定位置。

大家可以尝试设置上面的参数来达到想要的效果。

综述

通过jQZoom这个插件我们可以很方便地实现图片的局部放缩预览,希望对大家有帮助。

JavaScript

综述

我们肯定用过QQ空间吧,看到QQ空间里面的照片,点一下就会出现一个悬浮框,显示放大后的图片,而且可以点击左右箭头来查看上一张和下一张照片,怎样?这种效果,想不想实现一下。 在这里,我们就引用一个jQuery插件来帮助我们完成这件事情,让我们拭目以待吧

在线演示

我们首先来在线演示一下效果,然后我们说一下是怎样的实现,点开链接进行预览吧 在线预览

插件文件

其中包含了两个JS文件,一个是jQuery库,另一个就是lightbox插件文件。另外还有一个css文件,主要作用是给图片浏览器规定样式

HTML

我们写一个DEMO,包含了六张图片,用一个ul列表呈现出来

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
54
55
56
57
58
59
60
61
62
<!DOCTYPE>
<html>
<head>
<meta charset="utf-8"/>
<title>notesforlightbox实例</title>
<script src="js/jquery-1.4.2.min.js"></script>
<script src="js/jquery.notesforlightbox.js"></script>
<link rel="stylesheet" type="text/css" href="css/jquery.notesforlightbox.css" />
<style type="text/css">
body{font-size:13px}
.divFrame{width:380px;border:solid 1px #666}
.divFrame .divTitle{padding:5px;background-color:#eee;font-weight:bold}
.divFrame .divContent{padding:8px;line-height:1.6em}
.divFrame .divContent .divPics{background-color: #777;padding: 10px;width: 344px}
.divFrame .divContent .divPics ul{list-style: none;padding:0px;margin:0px}
.divFrame .divContent .divPics ul li{display: inline;}
.divFrame .divContent .divPics ul img{border: 5px solid #444;border-width: 5px;width:100px;height:100px}
.divFrame .divContent .divPics ul a:hover img{border:5px solid #fff;border-width: 5px;color: #fff;}
.divFrame .divContent .divPics ul a:hover{color: #fff;}
</style>
<script type="text/javascript">
$(function() {
$('.divPics a').lightBox({
overlayBgColor: "#666", //浏览图片时的背景色
overlayOpacity: 0.5, //背景色的透明度
containerResizeSpeed: 600 //图片切换时的速度;
})
})
</script>
</head>
<body>
<div class="divFrame">
<div class="divTitle">
我的相册
</div>
<div class="divContent">
<div class="divPics">
<ul>
<li><a href="images/img01.jpg" title="第1篇风景图片">
<img src="images/img01.jpg" alt="" />
</a></li>
<li><a href="images/img02.jpg" title="第2篇风景图片">
<img src="images/img02.jpg" alt="" />
</a></li>
<li><a href="images/img03.jpg" title="第3篇风景图片">
<img src="images/img03.jpg" alt="" />
</a></li>
<li><a href="images/img04.jpg" title="第4篇风景图片">
<img src="images/img04.jpg" alt="" />
</a></li>
<li><a href="images/img05.jpg" title="第5篇风景图片">
<img src="images/img05.jpg" alt="" />
</a></li>
<li><a href="images/img06.jpg" title="第6篇风景图片">
<img src="images/img06.jpg" alt="" />
</a></li>
</ul>
</div>
</div>
</div>
</body>
</html>

其中images文件夹中存在了六张图片,其中每一个中的超链接的href链接是一张图片,超链接中还包含了img标签。这样可以保证点击图片的时候可以呼出一张大图。

功能简析

1
2
3
4
5
6
7
$(function() {
$('.divPics a').lightBox({
overlayBgColor: "#666", //浏览图片时的背景色
overlayOpacity: 0.5, //背景色的透明度
containerResizeSpeed: 600 //图片切换时的速度;
})
})

我们取到了所有的超链接元素,然后调用了lightBox方法,参数是一系列集合,在这里定义了

1
2
3
overlayBgColor: "#666", //浏览图片时的背景色
overlayOpacity: 0.5, //背景色的透明度
containerResizeSpeed: 600 //图片切换时的速度;

在这里给出所有的参数说明

名称

默认值

说明

overlayBgColor

000

背景色

overlayOpacity

0.8

背景色透明度

fixedNavigation

false

是否始终显示上一张、下一张按钮

imageLoading

images/lightbox-ico-loading.gif

加载图片时显示的图片

imageBtnPrev

images/lightbox-btn-prev.gif

上一张按钮的图片

imageBtnNext

images/lightbox-btn-next.gif

下一张按钮的图片

imageBtnClose

images/lightbox-btn-close.gif

关闭按钮的图片

imageBlank

images/lightbox-blank.gif

上一张、下一张按钮周围空白部分的图片(默认透明)

containerBorderSize

10

展示图片的边框宽度

containerResizeSpeed

400

展示过程切换的速度

txtImage

Image

页码辅助文字

txtOf

of

页码辅助文字

keyToClose

c

关闭展示的快捷键

keyToPrev

p

上一张的快捷键

keyToNext

n

下一张的快捷键

值得注意的地方是

imageLoading

‘images/lightbox-ico-loading.gif’

加载图片时显示的图片

imageBtnPrev

‘images/lightbox-btn-prev.gif’

上一张按钮的图片

imageBtnNext

‘images/lightbox-btn-next.gif’

下一张按钮的图片

imageBtnClose

‘images/lightbox-btn-close.gif’

关闭按钮的图片

这几张张图片,我们如果不定义,则会使用JS中默认的定义路径 在 jquery.notesforlightbox.js 中,如下程序便实现了上一张图片和下一张图片等按钮的定义

1
2
3
4
5
6
7
8
9
imageLoading: 'images/loading.gif',
imageBtnPrev: 'images/prev.png',
imageBtnNext: 'images/next.png',
imageBtnClose: 'images/close.png',
imageBlank: 'images/lightbox-blank.gif',
imageBtnBottomPrev: 'images/btm_prev.gif',
imageBtnBottomNext: 'images/btm_next.gif',
imageBtnPlay: 'images/start.png',
imageBtnStop: 'images/pause.png',

所以,如果发现图片不正常显示可以检查一下这里的路径设置问题

代码下载

源码下载 代码已部署在GitHub,可以下载查看

总结

通过这个插件我们可以方便地实现图片的加载预览,效果也比较酷炫,希望对大家有帮助!

JavaScript

综述

我想大家一定见到过,在某个网站填写邮箱的时候,还没有填写完,就会出现一系列下拉列表,帮你自动补全邮箱的功能。现在我们就用jQuery来实现一下。 博主原创代码,如有代码写的不完善的地方还望大家多多指教。

功能简述

  • 填写邮箱名字,出现下拉列表,自动补全邮箱
  • 点击上下按键,选取下拉列表邮箱
  • 按回车键,选中列表内容,隐藏下拉列表
  • 鼠标经过,下拉列表选项设置为高亮
  • 鼠标点击,选中下拉列表选项,隐藏下拉列表

在线演示

在此直接插入一个iframe进行演示 链接为 在线演示

HTML

HTML代码很简单,我们就一个简单的输入框,然后一个ul标签,在内部可以放好多li标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<meta charset="utf-8"/>
<script src="js/jquery.min.js"></script>
<script src="js/main.js"></script>
<link href="css/style.css" rel="stylesheet"/>
</head>
<body>
<div class="content">
<input type="text" name="email" id="email" placeholder="请输入您的邮箱"/>
<ul class="list"></ul>
</div>
</body>
</html>

以上便是HTML代码

CSS

在CSS中,定义也比较简单,其中有一个 lilight 的 class,可以使背景变色,通过 remove 和 add 这个 class,我们可以轻松地实现下拉列表元素是否选中的区分。 CSS所有样式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.content input{
padding:5px 10px;
width:200px;
}
ul.list{
list-style:none;
padding:0px;
margin:0px;
overflow:hidden;
}
ul.list li{
border:1px solid #EEE;
width:180px;
padding:5px 10px;
margin:0px;
text-overflow:ellipsis; //溢出时变为省略
overflow:hidden;
}
.lilight{
background-color:#fafafa;
}

以上便是CSS代码

JS

我们引入 jQuery 来实现对元素的操作,实现了按键和鼠标监听,代码如下

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
$(function(){
//声明所有的电子邮件变量
var mail=new Array("sina.com.cn","126.com","163.com","gmail.com","qq.com","vip.qq.com","hotmail.com","sohu.com","139.com","vip.sina.com","cuiqingcai.com");
//生成一个个li,并加入到ul中
for(var i=0;i<mail.length;i++){
var liElement=$("<li class=\"autoli\"><span class=\"ex\"></span><span class=\"at\">@</span><span class=\"tail\">"+mail[i]+"</span></li>");
liElement.appendTo("ul.list");
}
//首先让list隐藏起来
$("ul.list").hide();

$("#email").keyup(function(event){
//键入的内容不是上下箭头和回车
if(event.keyCode!=38&&event.keyCode!=40&&event.keyCode!=13){
//如果输入的值不是空或者不以空格开头
if($.trim($(this).val())!=""&& $.trim($(this).val()).match(/^@/)==null){
$("ul.list").show();
//如果当前有已经高亮的下拉选项卡,那么将其移除
if($("ul.list li:visible").hasClass("lilight")){
$("ul.list li").removeClass("lilight");
}
//如果还存在下拉选项卡,那么将其高亮
if($("ul.list li:visible")){
$("ul.list li:visible:eq(0)").addClass("lilight");
}
}else{
//否则不进行显示
$("ul.list").hide();
$("ul.list li").removeClass("lilight");
}
//输入的内容还没有包括@符号
if($.trim($(this).val()).match(/.*@/)==null){
$(".list li .ex").text($(this).val());
}else{
//输入的符号已经包含了@
var str = $(this).val();
var strs = str.split("@");
$(".list li .ex").text(strs[0]);
if($(this).val().length>=strs[0].length+1){
tail=str.substr(strs[0].length+1);
$(".list li .tail").each(function(){
//如果数组中的元素是以文本中的后缀开头,那么就显示,否则不显示
if(!($(this).text().match(tail)!=null&&$(this).text().indexOf(tail)==0)){
//隐藏其他的li
$(this).parent().hide();
}else{
//显示所在的li
$(this).parent().show();
}
});
}
}
}
//按了回车时,将当前选中的元素写入到文本框中
if(event.keyCode==13){
$("#email").val($("ul.list li.lilight:visible").text());
$("ul.list").hide();
}
});

//监听上下方向键
$("#email").keydown(function(event){
//下方向键按下了
if(event.keyCode==40){
if($("ul.list li").is(".lilight")){
if($("ul.list li.lilight").nextAll().is("li:visible")){
$("ul.list li.lilight").removeClass("lilight").next("li").addClass("lilight");
}
}
}
//下方向键按下了
if(event.keyCode==38){
if($("ul.list li").is(".lilight")){
if($("ul.list li.lilight").prevAll().is("li:visible")){
$("ul.list li.lilight").removeClass("lilight").prev("li").addClass("lilight");
}
}
}
});

//当鼠标点击某个下拉项时,选中该项,下拉列表隐藏
$("ul.list li").click(function(){
$("#email").val($(this).text());
$("ul.list").hide();
});

//当鼠标划过某个下拉项时,选中该项,下拉列表隐藏
$("ul.list li").hover(function(){
$("ul.list li").removeClass("lilight");
$(this).addClass("lilight");
});

//当鼠标点击其他位置,下拉列表隐藏
$(document).click(function(){
$("ul.list").hide();
});
});

以上便是 jQuery 代码

源码下载

源码下载

总结

其实还有一个比较强大的插件,叫autocomplete,同样可以实现下拉列表的自动补全,功能更加完善,如果大家有兴趣可以去试一下。不过感觉最常用的就是邮箱自动补齐,而且直接用 jQuery 就可以比较方便地实现,所以博主就没有使用autocomplete插件,而是自己写了一下,一来练习一下,二来对这种功能的实现了解得更加透彻。 大家也可以尝试下,希望小伙伴们有帮助,加油!

JavaScript

综述

validate是一个用来验证表单提交的插件,应用十分广泛,具有如下的几个功能

  • 自带了基本的验证规则
  • 提供了丰富的验证信息提示功能
  • 多种事件触发验证
  • 自定义验证规则

下面我们就来感受一下这个插件的强大之处吧

插件下载

在这里我们需要用到的插件文件有

一个是表单验证的主文件,另一个是设置中文提示的文件。

实例引入

我们先用一个小例子来感受一下使用 validate 插件的便捷之处,这个例子中加入了表单合法性验证和错误提示,代码如下

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
54
55
56
57
58
59
60
61
62
<!DOCTYPE>
<html>
<head>
<title>validate验证插件</title>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.min.js">
</script>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.validate.js">
</script>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.validate.messages_cn.js">
</script>
<link type="text/css" rel="stylesheet" href="http://res.cuiqingcai.com/jqplugins/validate/style.css"></link>
<script type="text/javascript">
$(function() {
$("#frmV").validate(
{
/*自定义验证规则*/
rules: {
username: { required: true, minlength: 6 },
email: { required: true, email: true }
},
/*错误提示位置*/
errorPlacement: function(error, element) {
error.appendTo(element.siblings("span"));
}
}
);
});
</script>
</head>
<body>
<form id="frmV" method="get" action="#">
<div class="divFrame">
<div class="divTitle">
请输入下列资料
</div>
<div class="divContent">
<div>
用户名:<br />
<input id="username" name="username"
type="text" class="txt" />
<font color="red">*</font><br />
<span></span>
</div>
<div>
邮箱:<br />
<input id="email" name="email"
type="text" class="txt" />
<font color="red">*</font><br />
<span></span>
</div>
</div>
<div class="divBtn">
<input id="sbtUser" type="submit"
value="提交" class="btn" />
</div>
</div>
</form>
</body>
</html>

运行结果如下 在这里我们定义了 rules 来控制表单的合法性,通过 errorPlacement 来控制错误的输出位置。

校验规则

下面我们详细说一下关于rules的相关知识,将校检规则总结如下

序号

规则

描述

1

required:true

必须输入的字段。

2

remote:”check.php”

使用 ajax 方法调用 check.php 验证输入值。

3

email:true

必须输入正确格式的电子邮件。

4

url:true

必须输入正确格式的网址。

5

date:true

必须输入正确格式的日期。日期校验 ie6 出错,慎用。

6

dateISO:true

必须输入正确格式的日期(ISO),例如:2009-06-23,1998/01/22。只验证格式,不验证有效性。

7

number:true

必须输入合法的数字(负数,小数)。

8

digits:true

必须输入整数。

9

creditcard:

必须输入合法的信用卡号。

10

equalTo:”#field”

输入值必须和 #field 相同。

11

accept:

输入拥有合法后缀名的字符串(上传文件的后缀)。

12

maxlength:5

输入长度最多是 5 的字符串(汉字算一个字符)。

13

minlength:10

输入长度最小是 10 的字符串(汉字算一个字符)。

14

rangelength:[5,10]

输入长度必须介于 5 和 10 之间的字符串(汉字算一个字符)。

15

range:[5,10]

输入值必须介于 5 和 10 之间。

16

max:5

输入值不能大于 5。

17

min:10

输入值不能小于 10。

比如我们针对 email 这个表单就可以定义为

1
email: { required: true, email: true }

针对url的这个输入表单就可以定义为

1
url: { required: true, url: true }

以上便是校验规则的相关内容。

消息提示

在 jquery.validate.js 这个文件中,定义了默认的消息提示,不过它是英文的提示,默认的提示如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
messages: {
required: "This field is required.",
remote: "Please fix this field.",
email: "Please enter a valid email address.",
url: "Please enter a valid URL.",
date: "Please enter a valid date.",
dateISO: "Please enter a valid date (ISO).",
dateDE: "Bitte geben Sie ein gültiges Datum ein.",
number: "Please enter a valid number.",
numberDE: "Bitte geben Sie eine Nummer ein.",
digits: "Please enter only digits",
creditcard: "Please enter a valid credit card number.",
equalTo: "Please enter the same value again.",
accept: "Please enter a value with a valid extension.",
maxlength: $.validator.format("Please enter no more than {0} characters."),
minlength: $.validator.format("Please enter at least {0} characters."),
rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."),
range: $.validator.format("Please enter a value between {0} and {1}."),
max: $.validator.format("Please enter a value less than or equal to {0}."),
min: $.validator.format("Please enter a value greater than or equal to {0}.")
},

比如,如果遇到 email 校验有问题,那么便会提示

1
Please enter a valid email address

不过我们通过引入 jquery.validate.messages_cn.js 这个文件,写入了如下代码,将默认的提示修改为中文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jQuery.extend(jQuery.validator.messages, {
required: "必选字段",
remote: "请修正该字段",
email: "请输入正确格式的电子邮件",
url: "请输入合法的网址",
date: "请输入合法的日期",
dateISO: "请输入合法的日期 (ISO).",
number: "请输入合法的数字",
digits: "只能输入整数",
creditcard: "请输入合法的信用卡号",
equalTo: "请再次输入相同的值",
accept: "请输入拥有合法后缀名的字符串",
maxlength: jQuery.format("请输入一个长度最多是 {0} 的字符串"),
minlength: jQuery.format("请输入一个长度最少是 {0} 的字符串"),
rangelength: jQuery.format("请输入一个长度介于 {0} 和 {1} 之间的字符串"),
range: jQuery.format("请输入一个介于 {0} 和 {1} 之间的值"),
max: jQuery.format("请输入一个最大为 {0} 的值"),
min: jQuery.format("请输入一个最小为 {0} 的值")
});

当然,以上的设置都是默认的提示,我们还可以通过 messages 来设置提示,举一个小例子,加入 messages 选项

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
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE>
<html>
<head>
<title>validate验证插件</title>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.min.js">
</script>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.validate.js">
</script>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.validate.messages_cn.js">
</script>
<link type="text/css" rel="stylesheet" href="http://res.cuiqingcai.com/jqplugins/validate/style.css">
<script type="text/javascript">
$(function() {
$("#frmV").validate(
{
/*自定义验证规则*/
rules: {
username: { required: true, minlength: 6 },
email: { required: true, email: true }
},
/*错误提示位置*/
errorPlacement: function(error, element) {
error.appendTo(element.siblings("span"));
},
messages: {
username: { required: "请输入姓名", minlength: "长度不可小于6" },
email: { required: "请输入电子邮件", email: "请输入正确格式" }
}
}
);
})
</script>
</head>
<body>
<form id="frmV" method="get" action="#">
<div class="divFrame">
<div class="divTitle">
请输入下列资料
</div>
<div class="divContent">
<div>
用户名:<br />
<input id="username" name="username"
type="text" class="txt" />
<font color="red">*</font><br />
<span></span>
</div>
<div>
邮箱:<br />
<input id="email" name="email"
type="text" class="txt" />
<font color="red">*</font><br />
<span></span>
</div>
</div>
<div class="divBtn">
<input id="sbtUser" type="submit"
value="提交" class="btn" />
</div>
</div>
</form>
</body>
</html>

运行结果如下

失败验证

1
2
3
  errorPlacement: function(error, element) {
error.appendTo(element.siblings("span"));
},

我们用 errorPlacement 来处理验证失败后的处理,方法有两个参数,一个是error,一个是element。 其中error是字符串,保存了messages中返回的错误信息,element是验证失败的input元素。 比如上面这一句

1
error.appendTo(element.siblings("span"));

就代表把错误加入到input元素同级的span元素中,从而在标签内部显示错误的内容。 其他的情况我们可以灵活处理。

成功验证

有失败就有成功,在这里我们可以用一个函数来实现成功的验证

1
2
3
success: function(label) {
label.html("OK");
}

这里的label指的是发生错误时那个标签,就是上面例子中的span,通过html()方法可以实现标签内容的变化。例如下面的例子

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<!DOCTYPE>
<html>
<head>
<title>validate验证插件</title>
<meta charset="utf-8"/>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.min.js">
</script>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.validate.js">
</script>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.validate.messages_cn.js">
</script>
<link type="text/css" rel="stylesheet" href="http://res.cuiqingcai.com/jqplugins/validate/style.css">
<script type="text/javascript">
$(function() {
$("#frmV").validate(
{
/*自定义验证规则*/
rules: {
username: { required: true, minlength: 6 },
email: { required: true, email: true }
},
/*错误提示位置*/
errorPlacement: function(error, element) {
error.appendTo(element.siblings("span"));
},
messages: {
username: { required: "请输入姓名", minlength: "长度不可小于6" },
email: { required: "请输入电子邮件", email: "请输入正确格式" }
},
success: function(label) {
label.html("OK");
}
}
);
})
</script>
</head>
<body>
<form id="frmV" method="get" action="#">
<div class="divFrame">
<div class="divTitle">
请输入下列资料
</div>
<div class="divContent">
<div>
用户名:<br />
<input id="username" name="username"
type="text" class="txt" />
<font color="red">*</font><br />
<span>呵呵</span>
</div>
<div>
邮箱:<br />
<input id="email" name="email"
type="text" class="txt" />
<font color="red">*</font><br />
<span></span>
</div>
</div>
<div class="divBtn">
<input id="sbtUser" type="submit"
value="提交" class="btn" />
</div>
</div>
</form>
</body>
</html>

上面就是验证成功之后的效果,在相应提示的地方会显示OK。

异步验证

有时候我们需要用到异步验证,我们可以在rules中加入remote进行远程验证,例子如下

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!DOCTYPE>
<html>
<head>
<title>validate验证插件</title>
<meta charset="utf-8"/>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.min.js">
</script>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.validate.js">
</script>
<script type="text/javascript"
src="http://res.cuiqingcai.com/js/jquery.validate.messages_cn.js">
</script>
<link type="text/css" rel="stylesheet" href="http://res.cuiqingcai.com/jqplugins/validate/style.css">
<script type="text/javascript">
$(function() {
$("#frmV").validate(
{
/*自定义验证规则*/
rules: {
username: { required: true, minlength: 6 },
phone: {
required: true,
remote:{
url: "check_phone.php", //后台处理程序
type: "post", //数据发送方式
dataType: "json", //接受数据格式
data: { //要传递的数据
phone: function() {
return $("#phone").val();
}
}
}
}
},
/*错误提示位置*/
errorPlacement: function(error, element) {
error.appendTo(element.siblings("span"));
},
messages: {
username: { required: "请输入姓名", minlength: "长度不可小于6" },
phone: { required: "请输入电话", remote: "请输入正确格式" }
},
success: function(label) {
label.html("OK");
}
}
);
})
</script>
</head>
<body>
<form id="frmV" method="get" action="#">
<div class="divFrame">
<div class="divTitle">
请输入下列资料
</div>
<div class="divContent">
<div>
用户名:<br />
<input id="username" name="username"
type="text" class="txt" />
<font color="red">*</font><br />
<span></span>
</div>
<div>
电话号码:<br />
<input id="phone" name="phone"
type="text" class="txt" />
<font color="red">*</font><br />
<span></span>
</div>
</div>
<div class="divBtn">
<input id="sbtUser" type="submit"
value="提交" class="btn" />
</div>
</div>
</form>
</body>
</html>

PHP处理程序,注意这里的返回值只能是true或者false,并且需要加引号。

1
2
3
4
5
6
7
8
<?php 
$phone = $_POST['phone'];
if((strlen($phone) != 11) || !(preg_match("/13[0123456789]{1}\d{8}|15[012356789]\d{8}|18[0123456789]\d{8}|17[0678]\d{8}|14[57]\d{8}/",$phone))){
echo "false";
}else{
echo "true";
}
?>

演示如下 上面就是进行ajax异步验证的处理方式

自定义方法

有时候我们需要自定义一些验证方法,我们就需要用到addMethod方法,介绍如下 addMethod(name,method,message)方法

参数name 是添加的方法的名字 参数method是一个函数,接收三个参数(value,element,param) value 是元素的值,element是元素本身 param是参数,我们可以用addMethod 来添加除built-in Validation methods 之外的验证方法

例如手机号码的验证如下

1
2
3
4
5
6
7
$.validator.addMethod("phone",function(value,element,params){
if((value.length != 11) || (!value.match(/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[0|6|7|8]|18[0-9])\d{8}$/))){
return false;
}else{
return true;
}
},"请输入正确的手机号");

使用时如下

1
2
3
4
5
rules:{
phone:{
required:true,phone:true
},
},

有一个字段,只能输一个字母,范围是a-f,写法如下

1
2
3
4
5
6
7
8
9
10
$.validator.addMethod(“af”,function(value,element,params){
if(value.length>1){
return false;
}
if(value>=params[0] && value<=params[1]){
return true;
}else{
return false;
}
},”必须是一个字母,且a-f”);

使用时如下

1
2
3
rules:{
username:{ af:["a","f"] }
},

以上便是自定义验证方法的方式

DeBug模式

开启DeBug模式后,不会进行提交,只需要在代码中加入

1
debug:true

即可,这样不论怎样,都不会提交表单,对于调试十分有用。

验证通过提交

在上面的例子中,我们没有设置表单验证通过之后才提交,通过加入以下代码,可以实现验证之后才提交的效果

1
2
3
submitHandler:function(form){ 
form.submit();
}

通过设置上面的内容,我们就可以避免验证不成功submit跳转了

忽略元素

有时候,我们想跳过某些元素不进行验证,可以通过加入如下代码来实现,举例如下

1
ignore:"input",

忽略所有input元素

1
ignore:"#username",

忽略id为username的元素

1
ignore:".input",

忽略所有class为input的元素

响应事件

在默认的响应事件是 submit 提交事件,我们还可以通过设置来改变事件的响应,比如失去焦点时验证等等,举例如下 Onubmit:类型 Boolean,默认 true,指定是否提交时验证。

1
$(".selector").validate({   	onsubmit:false })

onfocusout:类型 Boolean,默认 true,指定是否在获取焦点时验证。

1
$(".selector").validate({     onfocusout:false })

onkeyup:类型 Boolean,默认 true,指定是否在敲击键盘时验证。

1
$(".selector").validate({    onkeyup:false })

onclick:类型 Boolean,默认 true,指定是否在鼠标点击时验证(一般验证 checkbox、radiobox)。

1
$(".selector").validate({    onclick:false })

focusInvalid:类型 Boolean,默认 true。提交表单后,未通过验证的表单(第一个或提交之前获得焦点的未通过验证的表单)会获得焦点。

1
$(".selector").validate({    focusInvalid:false })

focusCleanup:类型 Boolean,默认 false。当未通过验证的元素获得焦点时,移除错误提示(避免和 focusInvalid 一起使用)。

1
$(".selector").validate({    focusCleanup:true })

上面的响应事件一般不太常用,仅作了解即可。

总结

以上便是jQuery插件validate的用法,利用好这款插件对于编写将有极其大的帮助,希望大家能好好学习!

个人随笔

4月4日,凌晨3点,我依旧无法入眠,心里想了很多,索性起床把现在的所想记录下来。也许,在一觉过后,这些情感或许就无法挥洒出来了。现在,借着那纯音乐,借着那静谧的夜,总结一下那逝去的三月。 三月一日开学,到现在已经有了一个月多一点的时间了,都说大三下是最难熬的一段时光,亲身经历之后才觉得真的是这样,这一个月,可以用浮躁两个字来形容。 浮躁之处在哪?学习,面试,备考,社交,做事还有其他,浮躁之气充斥在方方面面。 大三开始,不得不去考虑工作或是读研的事情,身边的朋友们,有的选择考研,有的选择工作。室友三个,选择的也都是工作。开学之初,各大公司的校招纷纷开始了,朋友们纷纷开始往各大公司投简历,BAT也不例外。当然BAT终究是BAT,不是省油的灯,一些朋友就在这条道路上遭遇了不少的坎坷,有的简历初审就被PASS,有的电面几轮之后也不幸失败,过程非常坎坷。其中有些朋友一份简历投上几十家公司,想碰运气,录用上哪一个是哪一个,而在我看来,这真的没有太大的意义。投一个公司,对该公司有全面的了解是至关重要,而且公司的理念文化适不适合自己去工作也是一个重要的因素,在我看来,这种碰运气的方法,实不可取。 而对于我自己,更加倾向于读研,鉴于自己的学习成绩还算不错,前百分之五,想申请一下保研,查找了各大学校的招生信息,对自己要复习和准备的东西也有了一些把握,可这一个月以来,自己难以静心去学习。其实说来,周围的朋友纷纷在准备找工作实习,而他们的焦点则就放在了填补一些职业相关的专业知识,掌握一些面试技巧,他们一轮轮的电面,也为自己的工作积累了经验。而我,也在想在大四的时候申请一份实习工作,就算没有拿到Offer,在面试的过程中也能积累一部分的经验,索性我也去试试吧。 制作了自己的一份简历,我也投上了BAT和一些其他公司,因为我本身对网页开发比较了解,尤其是后端开发,PHP部分。而像阿里这样的公司,他们的后端不是利用PHP处理的,所以我就报了前端开发的职位。但是自己的前端功底还不太强,所以我就开始补习JS,jQuery等等的知识,这一个月以来,大多数时间就在学习这些知识。对于找工作我可以说是试一试的态度,学习起来感觉沉不住气,而且可能觉得随时会有一个电话面试打过来,所以我觉得难以系统地,静心地去学习这些知识。 其实周围的朋友和我的情况是差不多的,我特意问过他们最近是怎样的状态,他们也回应说,感觉有些浮躁,完全是在一个应试的压力下在学习,对于一些知识只是修修补补,没有一个系统的把握。仔细想想,真的是这样,他们的路就是工作,谁也想进一个更好的公司,所以他们也在尽力提升着自己,可事实却是,太浮躁。 除了这些,我真的觉得在其他方面也应该好好反思一下自己。 这一个月的作息非常乱,在寒假时曾经做过打算,早睡早起,可慢慢地,睡得越来越晚,同样,起得也越来越晚。开学的前几天,坚持12点之前入睡,可慢慢地,拖到凌晨一两点钟才睡,而理所应当地,早上就难以起床,早起了也还是觉得萎靡不振。最初课程比较少,而且都集中在周二这一天,所以,除了周二,几乎每天都有机会睡个懒觉,由于这个原因,自己也经常早上起得很晚,有时甚至起床就是中午吃饭的时间了。这么一来,一上午的时光岂不是白白浪费了吗?这样的结果归咎于什么?我想,是自己的毅力问题,难以持之以恒,难以律己。 开学以来一个最不让我后悔的决定就是,健身。曾经的时候,一天没有什么活动,就坐在那学习,娱乐,一坐一玩就是一整天,自己的身体确实也难以承受得了,处在一个亚健康状态。开学之初,和好朋友一起去了学校对面的健身馆去办了一张健身年卡,几乎每天我们都去练上一个多小时,互相督促,一起锻炼。我的目标是增肌,自己真的是太瘦了,在健身房,跑上几千米后,尝试各种健身器材,锻炼胸肌等等,另外还有一套腹肌撕裂者的动作,几乎每天都这么坚持做下来。一个月的时间说长也长,说短也短,自己本身太瘦,也在努力多吃,不过现在腹肌和胸肌还没有明显的迹象。不管怎样,坚持长期练下去吧,我相信会有成果的。 另外,在待人处事方面,还是觉得自己不够稳重,现在是21岁了,也该变得成熟些了。在言谈举止方面,现在回想起这一个月自己曾经做过的事,真的有些好幼稚,好可笑。我想,尝试着去变得更稳重,考虑事情更加周全,做事更加细致,结果一定更好。 过去的一个月,我大部分时间都是在宿舍,学习啊,娱乐啊,都是宅在那里,室友也是一样。现在觉得,的确是有些懒散了,而且懒散得效率低下,作息不规律。清明回去之后,是该换一种学习环境了,找一个安静的自习室,在那里看看书,敲敲代码。学习累了,听听音乐,出去走走,望一望周围同学努力的身影,去感受一下这样的状态,告别懒散的生活方式,大胆做一番新的尝试吧。而且,是时候为自己的保研做好好的准备了,专业课知识,英语,真的挺重要。 最后,想说几句话。 为了自己的梦想和所喜欢的事而努力,这份动力是发自内心的。 一个成熟的人,有一颗强大的内心,有一种强大的毅力,做事稳重,注重细节,让自己变得更加稳重和成熟吧。 在做每一件事之前,好好想想怎样可以把这件事做得更好,好好想想自己以后会不会为自己的所作而感到后悔。 让自己变得更优秀,好好经营自己,自己所想要的,说不定会随之而来。 现在凌晨四点十九,写完之后,心中一丝畅然,晚安,世界。

PHP

综述

1.什么是mysqli

PHP-MySQL 函数库是 PHP 操作 MySQL 资料库最原始的扩展库,PHP-MySQLi 的 i 代表 Improvement ,相当于前者的增强版,也包含了相对进阶的功能,另外本身也增加了安全性,比如可以大幅度减少 SQL 注入等问题的发生。

2. mysql与mysqli的概念相关

(1)mysql与mysqli都是php方面的函数集,与mysql数据库关联不大。 (2)在php5版本之前,一般是用php的mysql函数去驱动mysql数据库的,比如mysql_query()的函数,属于面向过程 (3)在php5版本以后,增加了mysqli的函数功能,某种意义上讲,它是mysql系统函数的增强版,更稳定更高效更安全,与mysql_query()对应的有mysqli_query(),属于面向对象,用对象的方式操作驱动mysql数据库

3. mysql与mysqli的主要区别

(1)mysql是非持继连接函数,mysql每次链接都会打开一个连接的进程,所以mysqli耗费资源少一些。 (2)mysqli是永远连接函数,mysqli多次运行mysqli将使用同一连接进程,从而减少了服务器的开销。mysqli封装了诸如事务等一些高级操作,同时封装了DB操作过程中的很多可用的方法。 (3)mysqli提供了面向对象编程方式和面向过程编程方式,而mysql则只可以面向过程。 例如如下代码分别是mysqli的面向对象编程方式和面向过程方式 面向对象方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$mysqli = new mysqli("localhost", "my_user", "my_password", "world");

/* check connection */
if (mysqli_connect_errno()) {
printf("Connect failed: %s\n", mysqli_connect_error());
exit();
}

printf("Host information: %s\n", $mysqli->host_info);

/* close connection */
$mysqli->close();
?>

面向过程方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$link = mysqli_connect("localhost", "my_user", "my_password", "world");

/* check connection */
if (!$link) {
printf("Connect failed: %s\n", mysqli_connect_error());
exit();
}

printf("Host information: %s\n", mysqli_get_host_info($link));

/* close connection */
mysqli_close($link);
?>

(4)mysqli 可以通过预处理语句来减少开销和SQL注入的风险,而mysql则做不到。 综上所述,如果大家用的是PHP5,而且mysql版本在5.0以上,希望大家以后能用mysqli的就尽量使用mqsqli,不仅高效,而且更安全,而且推荐大家使用面向对象编程方式。 在这里,我们也只介绍面向对象编程方式。

函数使用

1. 连接数据库并获取相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$mysqli=@new mysqli("localhost", "root", "", "mysql");
//如果连接错误
if(mysqli_connect_errno()){
echo "连接数据库失败:".mysqli_connect_error();
$mysqli=null;
exit;
}
//获取当前字符集
echo $mysqli->character_set_name()."<br>";
//获取客户端信息
echo $mysqli->get_client_info()."<br>";
//获取mysql主机信息
echo $mysqli->host_info."<br>";
//获取服务器信息
echo $mysqli->server_info."<br>";
//获取服务器版本
echo $mysqli->server_version."<br>";
//关闭数据库连接
$mysqli->close();
?>

如果连接成功则运行结果

latin1 mysqlnd 5.0.10 - 20111026 - $Id: e707c415db32080b3752b232487a435ee0372157 $ localhost via TCP/IP 5.6.12-log 50612

如果连接失败则可能结果为

连接数据库失败:Access denied for user ‘root’@’localhost’ (using password: YES) 连接数据库失败:Unknown database ‘hello’

2.查询数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$mysqli=@new mysqli("localhost", "root", "", "design");
//如果连接错误
if(mysqli_connect_errno()){
echo "连接数据库失败:".mysqli_connect_error();
$mysqli=null;
exit;
}
//构造SQL语句
$query = "SELECT * FROM designer order by ID LIMIT 3";
//执行SQL语句
$result = $mysqli->query($query);
//遍历结果
while($row = $result->fetch_array(MYSQLI_BOTH)){
echo "id".$row['id']."<br>";
}
//释放结果集
$result->free();
//关闭数据库连接
$mysqli->close();
?>

运行结果

1
2
3
id10062
id10063
id10064

在这里需要注意的是

1
fetch_array(MYSQLI_BOTH)

这个方法,参数有三个,分别是 MYSQLI_BOTH,MYSQLI_NUM,MYSQLI_ASSOC。 如果参数传入了 MYSQLI_BOTH,返回数组的索引既包括数字和名称。

1
2
3
4
5
6
7
8
9
array (size=26)
0 => string '10062' (length=5)
'id' => string '10062' (length=5)
1 => string '??' (length=2)
'name' => string '??' (length=2)
2 => string '1016903103@qq.com' (length=17)
'email' => string '1016903103@qq.com' (length=17)
3 => string '18366119732' (length=11)
'phone' => string '18366119732' (length=11)

如果参数传入了 MYSQLI_NUM,返回数组的索引只包含数字。

1
2
3
4
5
array (size=13)
0 => string '10062' (length=5)
1 => string '??' (length=2)
2 => string '1016903103@qq.com' (length=17)
3 => string '18366119732' (length=11)

如果参数传入了 MYSQLI_BOTH,返回数组的索引只包含名称。

1
2
3
4
5
array (size=13)
'id' => string '10062' (length=5)
'name' => string '??' (length=2)
'email' => string '1016903103@qq.com' (length=17)
'phone' => string '18366119732' (length=11)

其实还有等价的方法 fetch_row(),fetch_assoc()

他们之间的关系如下

$result->fetch_row() = mysql_fetch_row() = $result->fetch_array(MYSQLI_NUM) = mysql_fetch_array(MYSQLI_NUM) 返回索引数组

$result->fetch_assoc() = mysql_fetch_assoc() = $result->fetch_array(MYSQLI_ASSOC) = mysql_fetch_array(MYSQLI_ASSOC) 返回索引列名

如果 fetch_array()方法什么也不传,则默认传入了 MYSQLI_BOTH

3.插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$mysqli=@new mysqli("localhost", "root", "", "design");
//如果连接错误
if(mysqli_connect_errno()){
echo "连接数据库失败:".mysqli_connect_error();
$mysqli=null;
exit;
}
//插入数据
$sql="insert into designer(name,phone) values('hello','18352682923')";
//执行插入语句
$result=$mysqli->query($sql);
//如果执行错误
if(!$result){
echo "SQL语句有误<br>";
echo "ERROR:".$mysqli->errno."|".$mysqli->error;
exit;
}
//如果插入成功,则返回影响的行数
echo $mysqli->affected_rows;
//关闭数据库连接
$mysqli->close();
?>

如果插入成功,那么结果则会是1,如果失败,则会报出错误。

4.修改内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$mysqli=@new mysqli("localhost", "root", "", "design");
//如果连接错误
if(mysqli_connect_errno()){
echo "连接数据库失败:".mysqli_connect_error();
$mysqli=null;
exit;
}
//插入数据
$sql="update designer set name = 'hello' where id = 10062";
//执行插入语句
$result=$mysqli->query($sql);
//如果执行错误
if(!$result){
echo "SQL语句有误<br>";
echo "ERROR:".$mysqli->errno."|".$mysqli->error;
exit;
}
//如果插入成功,则返回影响的行数
echo $mysqli->affected_rows;
//关闭数据库连接
$mysqli->close();
?>

如果修改成功,同样返回被修改的行数。

5.预处理语句

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
<?php
$mysqli = @new mysqli("localhost", "root", "", "design");
//如果连接错误
if(mysqli_connect_errno()){
echo "连接数据库失败:".mysqli_connect_error();
$mysqli=null;
exit;
}
//准备好一条语句放到服务器中,插入语句
$sql = "insert into designer(name, email) values(?, ?)";
//生成预处理语句
$stmt = $mysqli->prepare($sql);
//给占位符号每个?号传值(绑定参数) i d s b,第一个参数为格式化字符,ss代表两个字符串,d代表数字
$stmt->bind_param("ss", $name, $email);
//为变量赋值
$name = "Mike";
$email = "mike@live.cn";
//执行
$stmt->execute();
//为变量赋值
$name = "Larry";
$email = "larry@live.cn";
//执行
$stmt->execute();
//最后输出
echo "最后ID".$stmt->insert_id."<br>";
echo "影响了".$stmt->affected_rows."行<br>";
//关闭数据库连接
$mysqli->close();
?>

通过以上的预处理语句,我们也可以实现数据插入。 那么预处理语句有什么特点呢?

1. 效率上更高, 就是如果执行多次相同的语句,只有语句数据不同, 因为将一条语句在服务器端准备好,然后将不同的值传给服务器,再让这条语句执行。相当于编译一次,使用多次。 2. 安全上:可以防止SQL注入(? 占位)这样就可以防止非正常的变量的注入。

所以,推荐大家使用mysqli的预处理语句的方式,不仅效率高,而且更加安全。

综述

以上就是对mysqli的一些方法的介绍,更加详细的内容,请查看 PHP 手册。 希望对大家有帮助!

PHP

综述

对于PHP的图像处理来说,应用最广泛的便是验证码处理了,上一节我们学习到了PHP绘图的一些基本操作。现在我们实际运用一下,来感受一下验证码的相关应用。 在这里,我们将整个验证码写成了一个PHP类,以后我们用的时候直接调用这个类就好了。 传入的参数为验证码的宽度,高度,还有验证码的字符。

验证码类

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<?php

class Code {
//验证码的宽度
private $width;
//验证码的高度
private $height;
//验证码字符的个数
private $codeNum;
//图像资源
private $image;
//干扰点的个数
private $disturbColorNum;
//验证码字符内容
private $checkCode;

//构造方法,默认宽为80,高为20,字符个数为4
function __construct($width=80, $height=20, $codeNum=4){
//赋值成员变量
$this->width=$width;
$this->height=$height;
$this->codeNum=$codeNum;
//获得验证码字符内容
$this->checkCode=$this->createCheckCode();
//设置干扰点的个数
$number=floor($width*$height/15);
if($number > 240-$codeNum){
$this->disturbColorNum= 240-$codeNum;
}else{
$this->disturbColorNum=$number;
}
}

//通过访问该方法向浏览器中输出图像
function showImage($fontFace=""){
//第一步:创建图像背景
$this->createImage();
//第二步:设置干扰元素
$this->setDisturbColor();
//第三步:向图像中随机画出文本
$this->outputText($fontFace);
//第四步:输出图像
$this->outputImage();
}

//通过调用该方法获取随机创建的验证码字符串
function getCheckCode(){
return $this->checkCode;
}

//设置图像资源
private function createImage(){
//创建图像资源
$this->image=imagecreatetruecolor($this->width, $this->height);
//随机背景色
$backColor=imagecolorallocate($this->image, rand(225, 255), rand(225,255), rand(225, 255));
//为背景添充颜色
imagefill($this->image, 0, 0, $backColor);
//设置边框颜色
$border=imagecolorallocate($this->image, 0, 0, 0);
//画出矩形边框
imagerectangle($this->image, 0, 0, $this->width-1, $this->height-1, $border);
}

//创建干扰元素
private function setDisturbColor(){
//创建干扰的点
for($i=0; $i<$this->disturbColorNum; $i++){
$color=imagecolorallocate($this->image, rand(0, 255), rand(0, 255), rand(0, 255));
imagesetpixel($this->image, rand(1, $this->width-2), rand(1, $this->height-2), $color);
}
//创建干扰的线条
for($i=0; $i<10; $i++){
$color=imagecolorallocate($this->image, rand(200, 255), rand(200, 255), rand(200, 255));
imagearc($this->image, rand(-10, $this->width), rand(-10, $this->height), rand(30, 300), rand(20, 200), 55, 44, $color);
}
}

//创建随机验证码
private function createCheckCode(){
$code="23456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKMNPQRSTUVWXYZ";
$string='';
//从字符串中取出随机的字符
for($i=0; $i < $this->codeNum; $i++){
$char=$code{rand(0, strlen($code)-1)};
$string.=$char;
}
//返回字符内容
return $string;
}

//设置验证码的字符
private function outputText($fontFace=""){
for($i=0; $i<$this->codeNum; $i++){
$fontcolor=imagecolorallocate($this->image, rand(0, 128), rand(0, 128), rand(0, 128));
if($fontFace==""){
$fontsize=rand(3, 5);
$x=floor($this->width/$this->codeNum)*$i+3;
$y=rand(0, $this->height-15);
imagechar($this->image,$fontsize, $x, $y, $this->checkCode{$i},$fontcolor);
}else{
$fontsize=rand(12, 16);
$x=floor(($this->width-8)/$this->codeNum)*$i+8;
$y=rand($fontSize+5, $this->height);
imagettftext($this->image,$fontsize,rand(-30, 30),$x,$y ,$fontcolor, $fontFace, $this->checkCode{$i});
}
}
}

//输出验证码图像资源
private function outputImage() {
if(imagetypes() & IMG_GIF){
header("Content-Type:image/gif");
imagepng($this->image);
}else if(imagetypes() & IMG_JPG){
header("Content-Type:image/jpeg");
imagepng($this->image);
}else if(imagetypes() & IMG_PNG){
header("Content-Type:image/png");
imagepng($this->image);
}else if(imagetypes() & IMG_WBMP){
header("Content-Type:image/vnd.wap.wbmp");
imagepng($this->image);
}else{
die("PHP不支持图像创建");
}
}
//析构方法
function __destruct(){
//销毁图像资源
imagedestroy($this->image);
}
}

以上便是我们的验证码类的全部实现,保存文件名为 code.class.php, 下面我们来看一下怎样应用。

实际应用

我们写好了这个类之后,该怎么来调用呢?我们写一个demo如下

1
2
3
4
5
6
7
<?php
session_start();
include "code.class.php";
$code=new Code(80, 20, 4);
$code->showImage(); //输出到页面中供 注册或登录使用
$_SESSION["code"]=$code->getCheckCode(); //将验证码保存到服务器中
?>

这段代码声明了一个code对象,然后调用 showImage 方法,显示出验证码,最后通过getCheckCode 方法,获得验证码字符串的内容,保存到了 session 全局变量中。 以上代码我们保存成 code.php 文件 之后,我们写一个表单来提交验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
session_start();
if(@$_POST["code"]&&strtoupper($_POST["code"])==strtoupper($_SESSION["code"])){
echo "ok";
}else{
echo "error";
}
?>
<body>
<form action="login.php" method="post">
code: <input type="text" name="code"> <img src="code.php" onclick="this.src='code.php?'+Math.random()"><br>
<input type="submit" name="sub" value="login"><br>
</form>
</body>

上面的代码包括了一个输入框还有验证码,验证码的 src 资源可以直接引用为 code.php 文件,这里需要注意的是,后面要加一个参数,防止因为浏览器缓存原因而导致验证码无法更换。 最后表单是提交到文件本身,所以就可以比较提交的数据和 session 全局变量之间的关系,如果相同输出 ok,如果不同输出 error 小伙伴可以验证一下!希望对大家有帮助!

PHP

综述

PHP 的图像处理主要有下面几个用途,一个是验证码的操作,另一个就是图像水印操作,这里我们一起来学习一下吧。

图像处理基础

1.总体流程

首先,我们必须先了解 PHP 图像处理的基本函数的用法。总体来说分为以下四个步骤

(1)创建画布,创建资源类型,指定 高度 宽度

1
2
resource imagecreate ( int x_size, int y_size )
resource imagecreatetruecolor ( int x_size, int y_size )

imagecreate() 返回一个图像标识符,代表了一幅大小为 x_size 和 y_size 的空白图像。不过推荐使用 imagecreatetruecolor(),这个是新建一个真彩色图像。

(2)绘制图像

制定各种颜色,矩形, 圆, 点, 线段, 扇形, 画字(字符, 字符串, freetype),每一个形状和字符绘制对应一个函数,这部分我们后面详细介绍。

(3)输出图像/保存处理好的图像

1
2
3
imagegif();
imagejpeg();
imagepng();

例如,imagegif() 则将其保存为 gif 格式的图像。 bool imagegif ( resource image [, string filename] )

imagegif() 从 image 图像以 filename 为文件名创建一个 GIF 图像。image 参数是 imagecreatetruecolor() 函数的返回值。 filename 参数为可选,如果省略,则原始图像流将被直接输出。通过 header() 发送 Content-type: image/gif 可以使 PHP 脚本直接输出 GIF 图像。

(4)释放资源

1
imagedestroy($img);

所以,我们创建图像的一般流程用代码可以总结如下

1
2
3
4
5
6
7
8
9
10
11
12
<?php
//创建图片资源
$img = imagecreatetruecolor( 200, 200);
//绘制图形
$red = imagecolorallocate($img, 0xFF, 0, 0);
imagechar($img, 5, 100, 100, "A", $red);
//输出图像
header("Content-Type:image/gif");
imagegif($img);
//释放资源
imagedestroy($img);
?>

输出的结果如下,它直接在浏览器中显示如下,背景默认为黑色。 20150328153220 如果我们不加 header,那么则会显示字符乱码,所以一定要记得下面这句话。

1
header("Content-Type:image/gif");

2.设置色彩

我们主要用到下面这个函数 int imagecolorallocate ( resource image, int red, int green, int blue ) imagecolorallocate() 返回一个标识符,代表了由给定的 RGB 成分组成的颜色。image 参数是 imagecreatetruecolor() 函数的返回值。red,green 和 blue 分别是所需要的颜色的红,绿,蓝成分。这些参数是 0 到 255 的整数或者十六进制的 0x00 到 0xFF。imagecolorallocate() 必须被调用以创建每一种用在 image 所代表的图像中的颜色。 例如下面的例子

1
2
3
4
5
6
7
8
9
10
<?php
//十进制方式
$red = imagecolorallocate($img, 0xFF, 0, 0);
$white = imagecolorallocate($im, 255, 255, 255);
$black = imagecolorallocate($im, 0, 0, 0);
// 十六进制方式
$gray = imagecolorallocate($img, 0xEE, 0xEE, 0xEE);
$white = imagecolorallocate($im, 0xFF, 0xFF, 0xFF);
$black = imagecolorallocate($im, 0x00, 0x00, 0x00);
?>

3.区域填充色彩

bool imagefill ( resource image, int x, int y, int color ) imagefill() 在 image 图像的坐标 x,y(图像左上角为 0, 0)处用 color 颜色执行区域填充(即与 x, y 点颜色相同且相邻的点都会被填充)。 好,让我们尝试一下填充背景色吧

1
2
3
4
5
6
7
8
9
10
11
12
<?php
//创建图片资源
$img = imagecreatetruecolor( 200, 200);
//绘制图形
$red = imagecolorallocate($img, 0xFF, 0, 0);
imagefill($img,0,0,$red);
//输出图像
header("Content-Type:image/gif");
imagegif($img);
//释放资源
imagedestroy($img);
?>

运行结果如下,背景被填充为了红色。 20150328154707

4.画各种元素

(1)画空心矩形

bool imagerectangle ( resource image, int x1, int y1, int x2, int y2, int col ) imagerectangle() 用 col 颜色在 image 图像中画一个矩形,其左上角坐标为 x1, y1,右下角坐标为 x2, y2。图像的左上角坐标为 0, 0。

(2)画填充矩形

bool imagefilledrectangle ( resource image, int x1, int y1, int x2, int y2, int color ) imagefilledrectangle() 在 image 图像中画一个用 color 颜色填充了的矩形,其左上角坐标为 x1,y1,右下角坐标为 x2,y2。0, 0 是图像的最左上角。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
//创建图片资源
$img = imagecreatetruecolor( 200, 200);
//声明颜色
$red=imagecolorallocate($img, 255, 0, 0);
$yellow=imagecolorallocate($img, 255, 255, 0);
$green=imagecolorallocate($img, 0, 255, 0);
$blue=imagecolorallocate($img, 0, 0, 255);
//填充背景
imagefill($img,0,0,$yellow);
//画一个矩形并填充
imagefilledrectangle($img, 10, 10, 80, 80, $green);
//画一个矩形
imagerectangle($img, 90, 10, 190, 80, $green);
//输出图像
header("Content-Type:image/gif");
imagegif($img);
//释放资源
imagedestroy($img);
?>

20150328155313

(3)画一条线段

bool imageline ( resource image, int x1, int y1, int x2, int y2, int color ) imageline() 用 color 颜色在图像 image 中从坐标 x1,y1 到 x2,y2(图像左上角为 0, 0)画一条线段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
//创建图片资源
$img = imagecreatetruecolor( 200, 200);
//声明颜色
$red=imagecolorallocate($img, 255, 0, 0);
$yellow=imagecolorallocate($img, 255, 255, 0);
$green=imagecolorallocate($img, 0, 255, 0);
$blue=imagecolorallocate($img, 0, 0, 255);
//填充背景
imagefill($img,0,0,$yellow);
//线段
imageline($img,0, 0, 200, 200 ,$blue);
imageline($img,200, 0, 0, 200, $blue);
//输出图像
header("Content-Type:image/gif");
imagegif($img);
//释放资源
imagedestroy($img);
?>

20150328155846

(4)画点

bool imagesetpixel ( resource image, int x, int y, int color ) imagesetpixel() 在 image 图像中用 color 颜色在 x,y 坐标(图像左上角为 0,0)上画一个点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
//创建图片资源
$img = imagecreatetruecolor( 200, 200);
//声明颜色
$red=imagecolorallocate($img, 255, 0, 0);
$yellow=imagecolorallocate($img, 255, 255, 0);
$green=imagecolorallocate($img, 0, 255, 0);
$blue=imagecolorallocate($img, 0, 0, 255);
//填充背景
imagefill($img,0,0,$yellow);
//点
imagesetpixel($img,50, 50 ,$red);
imagesetpixel($img,55, 50 ,$red);
imagesetpixel($img,59, 50 ,$red);
imagesetpixel($img,64, 50 ,$red);
imagesetpixel($img,72, 50 ,$red);
//输出图像
header("Content-Type:image/gif");
imagegif($img);
//释放资源
imagedestroy($img);
?>

20150328160304

(5)画空心圆

bool imageellipse ( resource image, int cx, int cy, int w, int h, int color ) imageellipse() 在 image 所代表的图像中画一个中心为 cx,cy(图像左上角为 0, 0)的椭圆。w 和 h 分别指定了椭圆的宽度和高度,椭圆的颜色由 color 指定。

(6)画实心圆

bool imagefilledellipse ( resource image, int cx, int cy, int w, int h, int color ) imagefilledellipse() 在 image 所代表的图像中以 cx,cy(图像左上角为 0, 0)为中心画一个椭圆。w 和 h 分别指定了椭圆的宽和高。椭圆用 color 颜色填充。如果成功则返回 TRUE,失败则返回 FALSE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
//创建图片资源
$img = imagecreatetruecolor( 200, 200);
//声明颜色
$red=imagecolorallocate($img, 255, 0, 0);
$yellow=imagecolorallocate($img, 255, 255, 0);
$green=imagecolorallocate($img, 0, 255, 0);
$blue=imagecolorallocate($img, 0, 0, 255);
//填充背景
imagefill($img,0,0,$yellow);
//圆
imageellipse($img, 100, 100, 100, 100,$green);
//实心圆
imagefilledellipse($img, 100, 100, 10, 10,$blue);
//输出图像
header("Content-Type:image/gif");
imagegif($img);
//释放资源
imagedestroy($img);
?>

20150328161138

(7)水平画一个字符

bool imagechar ( resource image, int font, int x, int y, string c, int color ) imagechar() 将字符串 c 的第一个字符画在 image 指定的图像中,其左上角位于 x,y(图像左上角为 0, 0),颜色为 color。如果 font 是 1,2,3,4 或 5,则使用内置的字体(更大的数字对应于更大的字体)。 bool imagestring ( resource image, int font, int x, int y, string s, int col ) imagestring() 用 col 颜色将字符串 s 画到 image 所代表的图像的 x,y 坐标处(这是字符串左上角坐标,整幅图像的左上角为 0,0)。如果 font 是 1,2,3,4 或 5,则使用内置字体。

(8)竖直画一个字符

bool imagecharup ( resource image, int font, int x, int y, string c, int color ) imagecharup() 将字符 c 垂直地画在 image 指定的图像上,位于 x,y(图像左上角为 0, 0),颜色为 color。如果 font 为 1,2,3,4 或 5,则使用内置的字体。 bool imagestringup ( resource image, int font, int x, int y, string s, int col ) imagestring() 用 col 颜色将字符串 s 垂直地画到 image 所代表的图像的 x, y 座标处(图像的左上角为 0, 0)。如果 font 是 1,2,3,4 或 5,则使用内置字体。

(9)带字体写入字符

array imagettftext ( resource image, float size, float angle, int x, int y, int color, string fontfile, string text )

image:图像资源。见 imagecreatetruecolor()。 size: 字体大小。根据 GD 版本不同,应该以像素大小指定(GD1)或点大小(GD2)。 angle: 角度制表示的角度,0 度为从左向右读的文本。更高数值表示逆时针旋转。例如 90 度表示从下向上读的文本。 x: 由 x,y 所表示的坐标定义了第一个字符的基本点(大概是字符的左下角)。这和 imagestring() 不同,其 x,y 定义了第一个字符的左上角。例如 “top left” 为 0, 0。 y: Y 坐标。它设定了字体基线的位置,不是字符的最底端。 color: 颜色索引。使用负的颜色索引值具有关闭防锯齿的效果。见 imagecolorallocate()。 fontfile: 是想要使用的 TrueType 字体的路径。 text: 文本字符串。

imagettftext() 返回一个含有 8 个单元的数组表示了文本外框的四个角,顺序为坐下角,右下角,右上角,左上角。这些点是相对于文本的而和角度无关,因此“左上角”指的是以水平方向看文字时其左上角。

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
<?php
//创建图片资源
$img = imagecreatetruecolor( 200, 200);
//声明颜色
$red=imagecolorallocate($img, 255, 0, 0);
$yellow=imagecolorallocate($img, 255, 255, 0);
$green=imagecolorallocate($img, 0, 255, 0);
$blue=imagecolorallocate($img, 0, 0, 255);
$navy=imagecolorallocate($img, 0, 0, 0x80);
//填充背景
imagefill($img,0,0,$yellow);
//画字符
imagechar($img, 2, 100, 100, "A", $red);
imagechar($img, 5, 120, 120, "B", $red);
imagecharup($img, 4, 60, 60, "C", $red);
imagecharup($img, 5, 80, 80, "D", $red);
imagestring($img, 3, 10, 10, "Hello", $navy);
imagestringup($img, 3, 10, 80, "Hello", $navy);
imagettftext($img, 25, 60, 150, 150, $blue, "simkai.ttf", "Hello");
imagettftext($img, 12, -60, 50, 150, $green, "simli.ttf", "Nihao");
//输出图像
header("Content-Type:image/gif");
imagegif($img);
//释放资源
imagedestroy($img);
?>

20150328163147

总结

以上我们总结了 PHP 图像绘图的相关函数的使用,在后面我们将应用于实际,比如验证码和图像水印操作中。 希望对大家有帮助!

HTML

综述

CSS3 已经变得非常流行,原本的 CSS 不支持自定义字体,但是传说中的 CSS3 基本上什么都可以,那么 CSS3 中可不可以自定义英文字体呢?这里我们就一起来感受一下。

语法规则

1
2
3
4
5
6
7
8
9
10
11
@font-face {

  font-family: 自定义的字体名称;

  src: 自定义的字体的存放路径;

  font-weight: normal;是否为粗体

  font-style: normal;定义字体样式,如斜体

}

取值说明

font-famliy

此值指的就是你自定义的字体名称,最好是使用你下载的默认字体,他将被引用到你的 Web 元素中的 font-family。如“font-family:”YourWebFontName”;”

source

此值指的是你自定义的字体的存放路径,可以是相对路径也可以是绝路径;

format

此值指的是你自定义的字体的格式,主要用来帮助浏览器识别,其值主要有以下几种类型:truetype,opentype,truetype-aat,embedded-opentype,avg 等

weight 和 style

这两个值大家一定很熟悉,weight 定义字体是否为粗体,style 主要定义字体样式,如斜体

各个浏览器需要字体的格式

TureTpe(.ttf)

.ttf 字体是 Windows 和 Mac 的最常见的字体,是一种 RAW 格式,因此他不为网站优化,支持这种字体的浏览器有 IE9+,Firefox3.5+,Chrome4+,Safari3+,Opera10+,iOS Mobile Safari4.2+

OpenType(.otf)

.otf 字体被认为是一种原始的字体格式,其内置在 TureType 的基础上,所以也提供了更多的功能,支持这种字体的浏览器有 Firefox3.5+,Chrome4.0+,Safari3.1+,Opera10.0+,iOS Mobile Safari4.2+

Web Open Font Format(.woff)

.woff 字体是 Web 字体中最佳格式,他是一个开放的 TrueType/OpenType 的压缩版本,同时也支持元数据包的分离,支持这种字体的浏览器有 IE9+,Firefox3.5+,Chrome6+,Safari3.6+,Opera11.1+

Embedded Open Type(.eot)

.eot 字体是 IE 专用字体,可以从 TrueType 创建此格式字体,支持这种字体的浏览器有 IE4+

SVG(.svg)

.svg 字体是基于 SVG 字体渲染的一种格式,支持这种字体的浏览器有 Chrome4+,Safari3.1+,Opera10.0+,iOS Mobile Safari3.2+ 所以,@font-face 中我们至少需要.woff,.eot 两种格式字体,甚至还需要.svg 等字体达到更多种浏览版本的支持。

综合写法

1
2
3
4
5
6
7
8
9
10
11
12
13
 @font-face {
    font-family: 'YourWebFontName';
    /* IE9 Compat Modes */
    src: url('YourWebFontName.eot');
    /* IE6-IE8 */
    src: url('YourWebFontName.eot?#iefix') format('embedded-opentype'),
        /* Modern Browsers */
        url('YourWebFontName.woff') format('woff'),
        /* Safari, Android, iOS */
        url('YourWebFontName.ttf'format('truetype'),
        /* Legacy iOS */
        url('YourWebFontName.svg#YourWebFontName') format('svg');
}

获取字体

在这里介绍一个网站,叫做 fontsquirrel

在这里,你可以通过上传你的字体,来获取上面四种格式的字体文件。

20150327005358

我们点击按钮 UPLOAD FONTS,选择本地的字体文件,然后网站就会为我们生成上述格式的字体文件,勾选 Agreement,然后直接点击下载即可,DOWNLOAD YOUR KIT。

20150327005550

比如我上传的字体名叫做 FuturaICG-Light,那么下载之后的文件目录就如下

20150327010127

其中,这个目录下给我们生成了一个 demo,可以用浏览器打开 html 后缀的文件,预览一下 demo 是怎么写的。

应用字体

如果我们要用,就把五个字体文件复制一下,复制到项目目录里。

20150327010641

然后在样式表 css 中加入如下代码即可生效啦,这个代码在 demo 的 stylesheet 文件中,我们直接复制即可,比如我的便是

1
2
3
4
5
6
7
8
9
10
11
@font-face {
font-family: 'futuraicg_lightregular';
src: url('FuturaICG-Light-webfont.eot');
src: url('FuturaICG-Light-webfont.eot?#iefix') format('embedded-opentype'),
url('FuturaICG-Light-webfont.woff2') format('woff2'),
url('FuturaICG-Light-webfont.woff') format('woff'),
url('FuturaICG-Light-webfont.ttf') format('truetype'),
url('FuturaICG-Light-webfont.svg#futuraicg_lightregular') format('svg');
font-weight: normal;
font-style: normal;
}

在这里要注意路径问题,如果 css 在字体的上级目录,那么就要在前面加上字体文件夹的名称,我想大家都能理解。 刷新一下页面,我们可以发现页面的字体效果就已经生效啦。 如果有不生效的地方,很可能是 CSS 表中设置了 html 或者 body 的 font-family 样式,在这里我们只需要把它们去掉即可。如图所示,把改行删掉即可。 20150327011119

这时,如果还不行,请检查路径设置。 以上就是我们用 CSS3 来自定义网页字体的方法,希望对大家有帮助。

Other

在远程主机上,我开启了 mysql 服务,用 phpmyadmin 可以打开,比如说用户名为 root,密码为 123456。不过用 Mysql 客户端远程连接时却报了错误,比如 Mysql-Front 报了如下错误。 Access denied for user ‘root’@’121.42.8.33’(using password:YES) 20150327004005 比较奇怪,phpmyadmin 可以正常访问,而 Mysql-Front 为什么无法连接呢?可能的原因,应该就是 IP 限制了,phpmyadmin 在连接时使用的是 localhost,而我们访问页面才使用的远程主机的 IP,而 Mysql-Front 连接的是远程主机。 解决方法如下, 首先修改 mysql 的配置文件,my.cnf,将

1
#bind-address = 127.0.0.1

这一行注释掉,要不然它永远限制了只能本机连接 然后我们需要新建一个用户,然后授予所有 IP 可以访问的权限就好啦。 在下面的 sql 语句中,username 即为用户名,password 为你要设置的密码。

1
2
3
4
5
6
7
CREATE USER 'username'@'localhost' IDENTIFIED BY 'password';

GRANT ALL PRIVILEGES ON *.* TO 'username'@'localhost' WITH GRANT OPTION;

CREATE USER 'username'@'%' IDENTIFIED BY 'password';

GRANT ALL PRIVILEGES ON *.* TO 'username'@'%' WITH GRANT OPTION;

通过执行以上语句,便创建了一个用户名为 username,密码为 password 的新账户,再用新账号登录,就可以连接成功啦。