互动性
Casey Reas 和 Ben Fry
屏幕在我们的身体和计算机内部的电路和电之间形成了一座桥梁。我们通过触摸板、轨迹球和操纵杆等多种设备控制屏幕上的元素,但键盘和鼠标仍然是台式计算机最常用的输入设备。计算机鼠标可以追溯到20世纪60年代末,当时道格拉斯·恩格尔巴特将该设备作为在线系统(NLS)的一个组成部分,NLS是最早配备视频显示器的计算机系统之一。鼠标的概念在施乐帕洛阿尔托研究中心(PARC)得到了进一步的发展,但它在1984年与苹果Macintosh(苹果Macintosh)一起推出,是它目前无处不在的催化剂。在过去的四十年里,鼠标的设计经历了多次修改,但其功能保持不变。在恩格尔巴特1970年的原始专利申请中,他把鼠标称为“X-Y位置指示器”,这仍然准确,但枯燥地定义了它的当代用途。
物理鼠标对象用于控制光标在屏幕上的位置并选择界面元素。光标位置由计算机程序读取为两个数字,即x坐标和y坐标。这些数字可以用来控制屏幕上元素的属性。如果收集和分析这些坐标,它们可以用来提取更高层次的信息,如鼠标的速度和方向。这些数据可以依次用于手势和模式识别。
键盘通常用于输入字符,用于编写文档、电子邮件和即时消息,但键盘有可能超出其原始用途。键盘从打字机到计算机的迁移扩展了它的功能,使它能够启动软件,在软件应用程序的菜单中移动,并在游戏中导航三维环境。在编写自己的软件时,您可以随心所欲地使用键盘数据。例如,手指的速度和节奏等基本信息可以由按键的速度来确定。这些信息可以控制事件的速度或运动质量。也可以忽略键盘上打印的字符,使用每个键相对于键盘网格的位置作为数字位置。
现代电脑键盘是打字机的直接后代。英语键盘上键的位置是从早期打字机继承来的。这种布局之所以称为QWERTY,是因为最上面一行字母键的顺序。这一百多年的机械遗产仍然影响着我们今天如何编写软件。
鼠标数据
处理变量mouseX和mouseY(注意大写的X和Y)将光标相对于原点的X坐标和Y坐标存储在显示窗口的左上角。要查看移动鼠标时产生的实际值,请运行此程序将值打印到控制台:
void draw() { frameRate(12); println(mouseX + " : " + mouseY); }
当程序启动时,mouseX和mouseY值为0。如果光标移动到显示窗口中,则将值设置为光标的当前位置。如果光标在左侧,则mouseX值为0,并且该值随着光标向右移动而增加。如果光标位于顶部,则mouseY值为0,并且该值随着光标向下移动而增加。如果在没有draw()的程序中使用mouseX和mouseY,或者在setup()中运行noLoop(),则值始终为0。
鼠标位置最常用于控制屏幕上视觉元素的位置。当视觉元素与鼠标值的关系不同时,会创建更有趣的关系,而不是简单地模拟当前位置。从鼠标位置添加和减去值会创建保持不变的关系,而将这些值相乘和相除会在鼠标位置和屏幕上的元素之间创建更改的视觉关系。在下面的第一个示例中,圆直接映射到光标,在第二个示例中,数字从光标位置相加和减去以创建偏移,在第三个示例中,乘法和除法用于缩放偏移。
void setup() { size(100, 100); noStroke(); } void draw() { background(126); ellipse(mouseX, mouseY, 33, 33); } |
void setup() { size(100, 100); noStroke(); } void draw() { background(126); ellipse(mouseX, 16, 33, 33); // 顶圆 ellipse(mouseX+20, 50, 33, 33); // 中间圆 ellipse(mouseX-20, 84, 33, 33); // 底圆 } |
void setup() { size(100, 100); noStroke(); } void draw() { background(126); ellipse(mouseX, 16, 33, 33); // 顶圆 ellipse(mouseX/2, 50, 33, 33); // 中间圆 ellipse(mouseX*2, 84, 33, 33); // 底圆 } |
要反转鼠标值,请从窗口宽度中减去mouseX值,然后从屏幕高度中减去mouseY值。
void setup() { size(100, 100); noStroke(); } void draw() { float x = mouseX; float y = mouseY; float ix = width - mouseX; // Inverse X float iy = height - mouseY; // Inverse Y background(126); fill(255, 150); ellipse(x, height/2, y, y); fill(0, 159); ellipse(ix, height/2, iy, iy); } |
处理变量pmouseX和pmouseY存储前一帧中的鼠标值。如果鼠标不移动,则值将相同,但如果鼠标移动很快,则值之间可能存在较大差异。若要查看差异,请运行以下程序并交替缓慢快速移动鼠标。观察值打印到控制台。
void draw() { frameRate(12); println(pmouseX - mouseX); }
从上一个鼠标位置到当前位置画一条线,在一帧中显示更改的位置,并显示鼠标的速度和方向。当鼠标不移动时,会绘制一个点,但是快速的鼠标移动会创建长线。
void setup() { size(100, 100); strokeWeight(8); } void draw() { background(204); line(mouseX, mouseY, pmouseX, pmouseY); } |
将mouseX和mouseY变量与if结构一起使用,以允许光标选择屏幕区域。以下示例演示光标在显示窗口的不同区域之间进行选择。第一个把屏幕分成两半,第二个把屏幕分成三分。
void setup() { size(100, 100); noStroke(); fill(0); } void draw() { background(204); if (mouseX < 50) { rect(0, 0, 50, 100); // Left } else { rect(50, 0, 50, 100); // Right } } |
void setup() { size(100, 100); noStroke(); fill(0); } void draw() { background(204); if (mouseX < 33) { rect(0, 0, 33, 100); // Left } else if (mouseX < 66) { rect(33, 0, 33, 100); // Middle } else { rect(66, 0, 33, 100); // Right } } |
使用逻辑运算符和if结构选择屏幕的矩形区域。如下面的示例所示,当创建一个关系表达式来测试矩形的每条边(左、右、上、下)并且这些边与逻辑“和”连接时,只有当光标位于矩形内时,整个关系表达式才为真。
void setup() { size(100, 100); noStroke(); fill(0); } void draw() { background(204); if ((mouseX > 40) && (mouseX < 80) && (mouseY > 20) && (mouseY < 80)) { fill(255); } else { fill(0); } rect(40, 20, 40, 60); } |
这段代码询问:“光标是否在左边缘的右侧,光标是否在右边缘的左侧,光标是否在上边缘之外,光标是否在下边缘之上?”?“下一个示例的代码将询问一组类似的问题,并将它们与关键字else结合起来,以确定哪个定义的区域包含光标。
void setup() { size(100, 100); noStroke(); fill(0); } void draw() { background(204); if ((mouseX <= 50) && (mouseY <= 50)) { rect(0, 0, 50, 50); // Upper-left } else if ((mouseX <= 50) && (mouseY > 50)) { rect(0, 50, 50, 50); // Lower-left } else if ((mouseX > 50) && (mouseY <= 50)) { rect(50, 0, 50, 50); // Upper-right } else { rect(50, 50, 50, 50); // Lower-right } } |
鼠标按钮
计算机鼠标和其他相关的输入设备通常有一个到三个按钮;处理过程可以检测这些按钮是用mousePressed和mouseButton变量按下的。与按钮状态一起使用时,光标位置使鼠标能够执行不同的操作。例如,当鼠标在图标上时按下按钮可以选择它,这样图标可以移动到屏幕上的其他位置。如果按下任何鼠标按钮,则mouse pressed变量为true;如果未按下鼠标按钮,则为false。变量mouse button是左、中或右,具体取决于最近按下的鼠标按钮。释放按钮后,mousePressed变量将立即恢复为false,但mouseButton变量将保留其值,直到按下其他按钮。这些变量可以单独使用,也可以组合使用来控制软件。运行这些程序以查看软件如何响应您的手指。
void setup() { size(100, 100); } void draw() { background(204); if (mousePressed == true) { fill(255); // White } else { fill(0); // Black } rect(25, 25, 50, 50); } |
void setup() { size(100, 100); } void draw() { if (mouseButton == LEFT) { fill(0); // Black } else if (mouseButton == RIGHT) { fill(255); // White } else { fill(126); // Gray } rect(25, 25, 50, 50); } |
void setup() { size(100, 100); } void draw() { if (mousePressed == true) { if (mouseButton == LEFT) { fill(0); // Black } else if (mouseButton == RIGHT) { fill(255); // White } } else { fill(126); // Gray } rect(25, 25, 50, 50); } |
并非所有的鼠标都有多个按钮,如果软件分布广泛,交互就不应该依赖于检测哪个按钮被按下。
键盘数据
处理记录最近按下的键以及当前是否按下某个键。如果按键,则布尔变量key pressed为true,否则为false。在if结构的测试中包含此变量,以便仅在按下键时才允许代码行运行。按住键时,keyPressed变量保持为true,只有在释放键时才变为false。
void setup() { size(100, 100); strokeWeight(4); } void draw() { background(204); if (keyPressed == true) { // 如果按键, line(20, 20, 80, 80); // 画一条线 } else { // 否则, rect(40, 40, 20, 20); // 绘制矩形 } } |
int x = 20; void setup() { size(100, 100); strokeWeight(4); } void draw() { background(204); if (keyPressed == true) { // 如果按键 x++; // 每次递增 1 给 x } line(x, 20, x-60, 80); } |
key变量存储单个字母数字字符。具体来说,它保存最近按下的键。这个键可以用text()函数显示在屏幕上(第150页)。
void setup() { size(100, 100); textSize(60); } void draw() { background(0); text(key, 20, 75); // 在坐标(20,75)处绘制 } |
key变量可用于确定是否按下了特定的键。下面的示例使用表达式key='A'来测试是否按下了A键。单引号表示A是char数据类型(第144页)。表达式key=“A”将导致错误,因为双引号将A表示为字符串,并且无法将字符串与char进行比较。逻辑和符号(&&operator)用于将表达式与key pressed变量连接,以确定按下的键是大写的A。
前面的示例使用大写字母A,但如果按小写字母,则不适用。要检查大写和小写字母,请使用逻辑或关系运算符扩展关系表达式。上一个程序中的第9行将更改为:
if((keyPressed==true)&&((key==a')| |(key==a')){
因为每个字符都有一个由ASCII表(第605页)定义的数值,所以键变量的值可以像任何其他数字一样用来控制视觉属性,例如形状元素的位置和颜色。例如,ASCII表将大写字母A定义为数字65,将数字1定义为49。
void setup() { size(100, 100); strokeWeight(4); } void draw() { background(204); // 如果按下“A”键,画一条线 if ((keyPressed == true) && (key == 'A')) { line(50, 25, 50, 75); } else { //否则,画一个椭圆 ellipse(50, 50, 50, 50); } } |
前面的示例使用大写字母A,但如果按小写字母,则不适用。要检查大写和小写字母,请使用逻辑或关系运算符扩展关系表达式。上一个程序中的第9行将更改为:
if((keyPressed==true)&&((key==a')||(key==a')){
因为每个字符都有一个由ASCII表(第605页)定义的数值,所以键变量的值可以像任何其他数字一样用来控制视觉属性,例如形状元素的位置和颜色。例如,ASCII表将大写字母A定义为数字65,将数字1定义为49。
void setup() { size(100, 100); stroke(0); } void draw() { if (keyPressed == true) { int x = key - 32; line(x, 0, x, height); } } |
float angle = 0; void setup() { size(100, 100); fill(0); } void draw() { background(204); if (keyPressed == true) { if ((key >= 32) && (key <= 126)) { // 如果键是字母数字,//将其值用作角度 angle = (key - 32) * 3; } } arc(50, 50, 66, 66, 0, radians(angle)); } |
编码密钥
除了读取数字、字母和符号的键值外,处理还可以从其他键(包括箭头键和Alt、Control、Shift、Backspace、Tab、Enter、Return、Escape和Delete键)读取值。变量keyCode将ALT、CONTROL、SHIFT、UP、DOWN、LEFT和RIGHT键存储为常量。在确定按下哪个编码键之前,必须先检查该键是否已编码。如果对键进行了编码,则表达式key==CODED为true,否则为false。即使不是字母数字,ASCII规范中包含的键(BACKSPACE、TAB、ENTER、RETURN、ESC和DELETE)也不会被标识为编码键。如果您正在进行跨平台项目,请注意,Enter键通常在pc和UNIX上使用,Return键在Macintosh上使用。检查Enter和Return以确保您的程序适用于所有平台(请参见代码12-17)。
int y = 35; void setup() { size(100, 100); } void draw() { background(204); line(10, 50, 90, 50); if (key == CODED) { if (keyCode == UP) { y = 20; } else if (keyCode == DOWN) { y = 50; } } else { y = 35; } rect(25, y, 50, 30); } |
事件
当按键或鼠标移动等动作发生时,一类称为事件的函数改变程序的正常流程。事件是程序正常流程的礼貌中断。按键和鼠标移动将一直存储到draw()结束,在这里它们可以执行不会干扰当前正在进行的绘图的操作。事件函数中的代码在每次相应事件发生时运行一次。例如,如果按下鼠标按钮,mousePressed()函数中的代码将运行一次,并且在再次按下按钮之前不会再次运行。这使得鼠标和键盘产生的数据可以独立于程序的其他部分进行读取。
鼠标事件
鼠标事件函数有mousePressed()、mouseReleased()、mouseMoved()和mouseDragged():
mousePressed() | 当按下鼠标按钮时,此块中的代码将运行一次 |
mouseReleased() | 此块中的代码在释放鼠标按钮时运行一次 |
mouseMoved() | 当鼠标移动时,此块中的代码将运行一次 |
mouseDragged() | 当按下鼠标按钮移动鼠标时,此块中的代码将运行一次 |
mousePressed()函数的工作方式与mousePressed变量不同。在释放鼠标按钮之前,mousePressed变量的值为true。因此,可以在draw()中使用它在按下鼠标时运行一行代码。相反,mousePressed()函数中的代码只在按下按钮时运行一次。这使得当鼠标单击用于触发操作(如清除屏幕)时非常有用。在下面的示例中,每次按下鼠标按钮时,背景值都会变亮。在您的计算机上运行该示例以查看响应于您的手指的更改。
int gray = 0; void setup() { size(100, 100); } void draw() { background(gray); } void mousePressed() { gray += 20; } |
下面的示例与上面的示例相同,但灰色变量是在mouserereled()事件函数中设置的,每次释放按钮时都会调用一次。只有运行程序并单击鼠标按钮才能看到这种差异。长时间按住鼠标按钮,注意只有松开按钮时背景值才会更改。
int gray = 0; void setup() { size(100, 100); } void draw() { background(gray); } void mouseReleased() { gray += 20; } |
在事件函数内部绘制通常不是一个好主意,但是可以在某些条件下完成。在绘制这些函数之前,必须考虑程序的流程。在本例中,正方形是在mousePressed()中绘制的,由于draw()中没有background(),因此它们仍保留在屏幕上。但是,如果使用background(),在鼠标事件函数中绘制的可视元素将仅显示在屏幕上一帧,或者默认情况下显示为1/60秒。事实上,您会注意到这个示例在draw()中没有任何内容,但它需要在那里强制处理才能继续监听事件。如果在draw()中运行background()函数,矩形将闪烁到屏幕上并消失。
void setup() { size(100, 100); fill(0, 102); } void draw() { } // Empty draw() 保持程序运行 void mousePressed() { rect(mouseX, mouseY, 33, 33); } |
mouseMoved()和mouseDragged()事件函数中的代码在鼠标位置发生更改时运行。当鼠标移动且未按下按钮时,mouseMoved()块中的代码将在每帧的末尾运行。当按下鼠标按钮时,mouseDragged()块中的代码也会执行相同的操作。如果鼠标在每帧之间保持相同的位置,则这些函数中的代码不会运行。在本例中,未按下按钮时灰色圆圈跟随鼠标,按下鼠标按钮时黑色圆圈跟随鼠标。
int dragX, dragY, moveX, moveY; void setup() { size(100, 100); noStroke(); } void draw() { background(204); fill(0); ellipse(dragX, dragY, 33, 33); // 黑色圆圈 fill(153); ellipse(moveX, moveY, 33, 33); // 灰色圆圈 } void mouseMoved() { // 移动灰色圆圈 moveX = mouseX; moveY = mouseY; } void mouseDragged() { // 移动黑色圆圈 dragX = mouseX; dragY = mouseY; } |
关键事件
每次按键都通过键盘事件函数keyPressed()和keyReleased()进行注册:
keyPressed() | 当按下任何键时,此块中的代码都会运行一次 |
keyReleased() | 当释放任何键时,此块中的代码将运行一次 |
每次按下一个键,key pressed()块中的代码都会运行一次。在此块中,可以测试按下了哪个键,并将此值用于任何目的。如果长时间按住某个键,则keyPressed()块中的代码可能会连续快速运行多次,因为大多数操作系统将接管并重复调用keyPressed()函数。开始重复所需的时间和重复的速度因计算机而异,具体取决于键盘的首选项设置。在本例中,当按下T键时,布尔变量drawT的值将从false设置为true;这将导致代码行开始运行draw()中的矩形。
boolean drawT = false; void setup() { size(100, 100); noStroke(); } void draw() { background(204); if (drawT == true) { rect(20, 20, 60, 20); rect(39, 40, 22, 45); } } void keyPressed() { if ((key == 'T') || (key == 't')) { drawT = true; } } |
每次释放密钥时,keyReleased()块中的代码都会运行一次。下面的示例基于前面的代码;每次释放键时,布尔变量drawT都被设置回false以阻止形状在draw()中显示。
boolean drawT = false; void setup() { size(100, 100); noStroke(); } void draw() { background(204); if (drawT == true) { rect(20, 20, 60, 20); rect(39, 40, 22, 45); } } void keyPressed() { if ((key == 'T') || (key == 't')) { drawT = true; } } void keyReleased() { drawT = false; } |
事件流
如前所述,用draw()编写的程序每秒向屏幕显示60帧。frameRate()函数用于设置每秒显示的帧数限制,noLoop()函数可用于停止draw()循环。当与鼠标和键盘事件函数结合使用时,附加函数loop()和redraw()提供了更多选项。如果程序已使用noLoop()暂停,则运行loop()将继续其操作。由于事件函数是使用noLoop()暂停程序时继续运行的唯一元素,因此可以在这些事件中使用loop()函数继续在draw()中运行代码。下面的示例在每次按下鼠标按钮时运行draw()函数约两秒钟,然后在该时间过后暂停程序。
int frame = 0; void setup() { size(100, 100); } void draw() { if (frame > 120) { // 如果从鼠标开始120帧 noLoop(); //被按下,停止程序 background(0); // 把背景调黑。 } else { // 否则,设置背景 background(204); // 浅灰色并画线 line(mouseX, 0, mouseX, 100); // 在鼠标位置 line(0, mouseY, 100, mouseY); frame++; } } void mousePressed() { loop(); frame = 0; }
函数的作用是:在draw()中运行一次代码,然后停止执行。当显示器不需要持续更新时,这很有帮助。下面的示例在每次按下鼠标按钮时运行draw()中的代码。
void setup() { size(100, 100); noLoop(); } void draw() { background(204); line(mouseX, 0, mouseX, 100); line(0, mouseY, 100, mouseY); } void mousePressed() { redraw(); // 在draw中运行代码一次 }
光标图标
光标可以用noCursor()函数隐藏,也可以用cursor()函数设置为显示为不同的图标或图像。当noCursor()函数运行时,光标图标在移动到显示窗口时消失。为了反馈光标在软件中的位置,可以使用mouseX和mouseY变量绘制和控制自定义光标。
void setup() { size(100, 100); strokeWeight(7); noCursor(); } void draw() { background(204); ellipse(mouseX, mouseY, 10, 10); } //如果运行noCursor(),则当程序运行时光标将被隐藏,直到运行cursor()函数来显示它为止。 void setup() { size(100, 100); noCursor(); } void draw() { background(204); if (mousePressed == true) { cursor(); } }
向cursor()函数添加一个参数,将其更改为另一个图标或图像。加载并使用图像,或使用自描述性选项:箭头、十字、手、移动、文本和等待。
void setup() { size(100, 100); } void draw() { background(204); if (mousePressed == true) { cursor(HAND); // 手绘光标 } else { cursor(CROSS); } line(mouseX, 0, mouseX, height); line(0, mouseY, width, mouseY); }
这些光标图标是计算机操作系统的一部分,在不同的计算机上会显示不同的图标。