目录
Part1拓展
拓展需求:
需求分步:
具体实现
1.实现多选的功能,包括多选之后的删除,创建,方法缩小都得进行进行拓展
2.实现ctrl+单击/“橡皮筋-套索”组合方法选择多个目标
3.最后对part2,part3中的撤销重做,剪切板等功能进行完善,实现对多选之后的操作的适配
此部分是基于对第一部分功能的拓展!
Part1拓展
拓展需求:
1.1 用户可以添加/删除单个目标/从选择通过控制点击
1.2 用户可以使用讲座中讨论的“橡皮筋-套索”组合方法选择多个目标
1.2.1 简化使用橡皮筋或套索的决定,以选择更多的项目
1.3 用户可以通过按下控制键和使用橡皮带套索控制来添加/删除多个目标
1.4 当多个目标被选中时,操作会影响所有选中的项目(移动,调整大小,删除)
需求分步:
1.实现多选的功能,包括多选之后的删除,创建,方法缩小都得进行进行拓展
2.实现ctrl+单击/“橡皮筋-套索”组合方法选择多个目标
3.最后对part2,part3中的撤销重做,剪切板等功能进行完善,实现对多选之后的操作的适配
具体实现
1.实现多选的功能,包括多选之后的删除,创建,方法缩小都得进行进行拓展
思路:实现多选功能就肯定需要一个列表对选中的目标进行存储,但之前的单选也应该保留,所以我就想到普通单选和多选可以分开存储,那么就必须保证单选和多选不会起冲突(不可以同时有单选和多选两种情况)
实现方法:
1.在iModel类中添加多选列表,并添加相关方法,包括增删改查以及Blob是否在列表中等方法。
public class InteractionModel {
Blob selected;
// 多选列表
List<Blob> selectedBlobs = new ArrayList<>();
public void setSelected(Blob b) {
clearSelectedBlobs();
selected = b;
notifySubscribers();
}
public void unselect() {
selected = null;
notifySubscribers();
}
public Blob getSelected() {
return selected;
}
// 多选列表操作的相关方法
public void setSelectedBlobs(List<Blob> blobs){
selectedBlobs = blobs;
notifySubscribers();
}
public void addSelectedBlobs(Blob blob){
selectedBlobs.add(blob);
notifySubscribers();
}
public void deleteSelectedBlob(Blob blob){
Iterator<Blob> it = selectedBlobs.iterator();
while (it.hasNext()) {
Blob s = it.next();
if (blob.equals(s)) {
it.remove();
}
}
notifySubscribers();
}
public void clearSelectedBlobs(){
selectedBlobs.clear();
notifySubscribers();
}
public List<Blob> getSelectedBlobs(){
return selectedBlobs;
}
public boolean containedInSelected(Blob blob){
Iterator<Blob> it = selectedBlobs.iterator();
while (it.hasNext()) {
Blob s = it.next();
if (blob.equals(s)) {
return true;
}
}
return false;
}
}
2.在View类中的画操作中添加对多选目标的显示
仅仅只需要对imodel的多选列表中的对象显示即可
public class BlobView extends StackPane implements BlobModelListener, IModelListener {
private void draw() {
gc.clearRect(0,0,myCanvas.getWidth(),myCanvas.getHeight());
model.getBlobs().forEach(b -> {
// 仅仅只需要对imodel的多选列表中的对象显示即可
if (b == iModel.getSelected() || iModel.containedInSelected(b)) {
gc.setFill(Color.TOMATO);
} else {
gc.setFill(Color.BEIGE);
}
gc.fillOval(b.x-b.r,b.y-b.r,b.r*2,b.r*2);
gc.setFill(Color.BLACK);
gc.fillText(String.valueOf(b.getCount()), b.x-3,b.y+3);
});
}
}
3.修改controller类中的多选操作以及对单选与多选逻辑上修改即可
鼠标按压时,判断按压背景还是某个圆,是某个圆,则就是选择操作
选择 从是否按压Ctrl上可以区分为多选还是单选。
若是多选 则通过判断该圆是否被选中后 对圆进行选中/取消(为了分开单选和多选,在多选时就要将单选取消)
若是单选 若此时多选不为空(即正在进行多选操作,就单选不成功)
为空 就可以正常使用
之后的每一个增加,删除,修改操作都是对两种情况进行判断
若单选 正常操作即可
多选 对多个目标边里进行操作即可
public class BlobController {
BlobModel model;
InteractionModel iModel;
double prevX,prevY;
double dX,dY;
enum State {READY,PREPARE_CREATE, DRAGGING, DRAGGING_TOOL}
State currentState = State.READY;
public void handlePressed(MouseEvent event) {
switch (currentState) {
case READY -> {
if (model.hitBlob(event.getX(),event.getY())) { // 选中⚪
Blob b = model.whichHit(event.getX(),event.getY());
// 对多选和单选就行区分
if (event.isControlDown()){
if (iModel.containedInSelected(b)){
iModel.deleteSelectedBlob(b);
}else {
iModel.addSelectedBlobs(b);
iModel.unselect();
}
}else if (iModel.getSelectedBlobs().isEmpty()){
iModel.setSelected(b);
}
prevX = event.getX();
prevY = event.getY();
currentState = State.DRAGGING; // 选中之后就改变状态为dragging
} else {
if (event.isControlDown()){
iModel.clearPoints();
iModel.setPathComplete(false);
System.out.println("设置起点");
iModel.setInitialPointX(event.getX());
iModel.setInitialPointY(event.getY());
iModel.addPoint(new Point2D(event.getX(), event.getY()));
System.out.println("起点为"+iModel.getInitialPointX()+","+iModel.getInitialPointY());
currentState = State.DRAGGING_TOOL;
} else {
currentState = State.PREPARE_CREATE; // 未选中 就意味着在释放时要创建新的⚪
}
}
}
}
}
public void handleDragged(MouseEvent event) { // 鼠标拖动
switch (currentState){
case PREPARE_CREATE -> {
currentState = State.READY;
}
case DRAGGING -> {
if (!event.isShiftDown()){
// 拖动被选中的⚪
dX = event.getX() - prevX;
dY = event.getY() - prevY;
prevX = event.getX();
prevY = event.getY();
if (iModel.getSelected() != null){
System.out.println("移动之前"+iModel.getSelected());
BlobAction blobAction = BlobAction.DraggedBlobRecord(iModel.getSelected());
model.moveBlob(iModel.getSelected(), dX,dY);
System.out.println("移动之后"+iModel.getSelected());
// 创建一个移动操作
List<BlobAction> blobActions = iModel.addBlobAction(iModel.createBlobActions(), blobAction);
iModel.registerUndoStack(blobActions);
iModel.clearRedoStack();
}else {
// 同时移动多个选中目标
List<BlobAction> blobActions = iModel.createBlobActions();
List<Blob> selectedBlobs = iModel.getSelectedBlobs();
selectedBlobs.forEach(b -> {
BlobAction blobAction = BlobAction.DraggedBlobRecord(b);
blobActions.add(blobAction);
});
model.moveBlobs(selectedBlobs, dX, dY);
// 创建多个移动操作
iModel.registerUndoStack(blobActions);
iModel.clearRedoStack();
System.out.println("多个移动栈"+blobActions);
}
}else{
// 更改⚪的大小
dX = event.getX() - prevX;
if (iModel.getSelected() != null){
Blob blob = iModel.getSelected();
BlobAction blobAction = BlobAction.MotifiedBlobRecord(blob);
model.motifyR(blob,dX);
// 创建更改大小操作
List<BlobAction> blobActions = iModel.addBlobAction(iModel.createBlobActions(), blobAction);
iModel.registerUndoStack(blobActions);
iModel.clearRedoStack();
}else {
List<BlobAction> blobActions = iModel.createBlobActions();
// 修改多选的多个圆大小
List<Blob> selectedBlobs = iModel.getSelectedBlobs();
selectedBlobs.forEach(b -> {
BlobAction blobAction = BlobAction.MotifiedBlobRecord(b);
blobActions.add(blobAction);
});
model.motifyRs(selectedBlobs, dX);
iModel.registerUndoStack(blobActions);
iModel.clearRedoStack();
System.out.println("多个更改大小栈"+blobActions);
}
}
}
}
}
public void handleReleased(MouseEvent event) { // 鼠标释放
switch (currentState) {
case PREPARE_CREATE -> { // 创建一个⚪
if (event.isShiftDown()){
Blob blob = model.addBlob(event.getX(), event.getY());
currentState = State.READY;
// 注册一个创建操作
List<BlobAction> blobActions = iModel.addBlobAction(iModel.createBlobActions(), new BlobAction(blob,"CREATE"));
iModel.registerUndoStack(blobActions);
iModel.clearRedoStack();
}else if (event.isControlDown()){
// 删除所有⚪
model.clear();
iModel.clearUndoStack();
iModel.clearRedoStack();
}else {
iModel.unselect();
iModel.clearSelectedBlobs();
}
}
case DRAGGING -> {
// iModel.unselect();
currentState = State.READY;
}
case DRAGGING_TOOL -> {
currentState = State.READY;
iModel.setPathComplete(true);
}
}
}
public void keyHandlePressed(KeyEvent event) { // 键盘输入
System.out.println(event.getCode());
List<BlobAction> blobActions;
if (event.getCode() == KeyCode.DELETE){
System.out.println("正在执行删除操作");
System.out.println(iModel.getSelected());
System.out.println(currentState);
if (iModel.getSelected() != null){
System.out.println("删除单个选中目标");
model.deleteBlob(iModel.getSelected());
// 注册删除操作 blob type
blobActions = iModel.addBlobAction(iModel.createBlobActions(), new BlobAction(iModel.getSelected(), "DELETE"));
iModel.registerUndoStack(blobActions);
iModel.clearRedoStack();
}else {
System.out.println("删除多个选中目标");
blobActions = iModel.createBlobActions();
List<Blob> selectedBlobs = iModel.getSelectedBlobs();
selectedBlobs.forEach(b -> {
BlobAction blobAction = new BlobAction(b,"DELETE");
blobActions.add(blobAction);
});
model.deleteBlobs(selectedBlobs);
iModel.registerUndoStack(blobActions);
iModel.clearRedoStack();
System.out.println("多个删除栈"+blobActions);
}
}
}
}
2.实现ctrl+单击/“橡皮筋-套索”组合方法选择多个目标
1.实现Ctrl + 单击 (上面其实已经实现了)
2.“橡皮筋-套索”组合方法选择多个目标
1.矩形
思路:在鼠标按压时,设置起始位置点,在拖动时,鼠标的位置就是终止点。这两个点就是矩形的对角点。然后通过这两个点在视图层中画出矩形
实现步骤:
1.在iModel类中添加起始点和游标位置的存储
设置游标点时,view层就要刷新,所以要添加检测方法dragNotifySubscribers();
public class InteractionModel {
// 起始位置
double initialPointX,initialPointY;
// 游标位置
double cursorX,cursorY;
public void setCursorXY(double cursorX,double cursorY) {
this.cursorX = cursorX;
this.cursorY = cursorY;
dragNotifySubscribers();
}
get/set方法。。。。
}
2.在View类中画出矩形,并设置动态检测方法
(1).在检测接口类添加检测方法
public interface IModelListener {
void iModelChanged();
void iModelDragChanged();
}
(2).在视图类中实现该方法并画出矩形
public class BlobView extends StackPane implements BlobModelListener, IModelListener {
private void drawDrag() {
// 矩形
if (!iModel.isPathComplete()){
gc.setStroke(Color.GREEN);
gc.strokeRect(Math.min(iModel.getInitialPointX(),iModel.getCursorX()),
Math.min(iModel.getInitialPointY(),iModel.getCursorY()),
Math.abs(iModel.getCursorX() - iModel.getInitialPointX()),
Math.abs(iModel.getCursorY() - iModel.getInitialPointY()));
}
}
@Override
public void iModelChanged() {
draw();
drawDrag();
}
@Override
public void iModelDragChanged() {
draw();
drawDrag();
}
}
(3)iModel类中设置检测方法(也就是第一步中用于监测的方法)
public class InteractionModel {
// 对矩形之外的图形进行监测
private void notifySubscribers() {
subscribers.forEach(s -> s.iModelChanged());
}
// 对矩形进行监测
private void dragNotifySubscribers() {
subscribers.forEach(s -> s.iModelDragChanged());
}
}
3.控制层中实现矩形多选圆的算法
该算法很简单就是判断圆心是否在矩形中,并且圆心到四条边的距离是否大于圆的半径即可判断
public boolean isBlobInStrokeRect(Blob blob){
if (!(blob.getX() > iModel.getInitialPointX() == blob.getX() < iModel.getCursorX())){
return false;
}
if (!(blob.getY() > iModel.getInitialPointY() == blob.getY() < iModel.getCursorY())){
return false ;
}
if (Math.abs(blob.getX() - iModel.getInitialPointX()) > blob.getR() && Math.abs(blob.getX() - iModel.getCursorX()) > blob.getR()
&& Math.abs(blob.getY() - iModel.getInitialPointY()) > blob.getR() && Math.abs(blob.getY() - iModel.getCursorY()) > blob.getR()){
return true;
}
return false;
}
4.就是在controller增重设置起始点与游标点即可
拖动时设置新的状态机 DRAGGING_TOOL 用于画橡皮筋和矩形时使用(和更改大小,拖动等操作区分开)
public class BlobController {
public void handlePressed(MouseEvent event) {
switch (currentState) {
case READY -> {
if (model.hitBlob(event.getX(),event.getY())) { // 选中⚪
.......... // 选中之后就改变状态为dragging
} else {
if (event.isControlDown()){
iModel.clearPoints();
iModel.setPathComplete(false);
System.out.println("设置起点");
// 按压时添加起始点
iModel.setInitialPointX(event.getX());
iModel.setInitialPointY(event.getY());
iModel.addPoint(new Point2D(event.getX(), event.getY()));
System.out.println("起点为"+iModel.getInitialPointX()+","+iModel.getInitialPointY());
currentState = State.DRAGGING_TOOL;
} else {
currentState = State.PREPARE_CREATE; // 未选中 就意味着在释放时要创建新的⚪
}
}
}
}
}
public void handleDragged(MouseEvent event) { // 鼠标拖动
switch (currentState) {
case DRAGGING_TOOL -> {
if (event.isControlDown()){
iModel.clearSelectedBlobs();
System.out.println("设置游标点");
iModel.setCursorXY(event.getX(), event.getY());
iModel.addPoint(new Point2D(event.getX(), event.getY()));
// 设置之后对所有圆遍历判断 是否在矩形中
model.getBlobs().forEach(b -> {
System.out.println("开始运行。。。");
if (isBlobInStrokeRect(b)){
// 是 则被选中
iModel.addSelectedBlobs(b);
}
if (isBlobInPoints(b)){
iModel.addSelectedBlobs(b);
}
});
}
}
}
}
}
2.橡皮筋-套索”
思路:就是将鼠标移动时的点全纪录下来,通过这些点判断圆是否被圈中
实现步骤:
1.将鼠标移动时的点记录在iModel类中,同时使用pathComplete来判断在鼠标移动过程以及鼠标按压/释放状态
刚按压时 pathComplete -> false
过程中 false
释放时 true
public class InteractionModel {
// 记录鼠标移动时的点
List<Point2D> points = new ArrayList<>();
boolean pathComplete;
public boolean isPathComplete() {
return pathComplete;
}
public void setPathComplete(boolean pathComplete) {
this.pathComplete = pathComplete;
dragNotifySubscribers();
if (pathComplete == true){
notifySubscribers();
}
}
public List<Point2D> getPoints() {
return points;
}
public void addPoint(Point2D point){
points.add(point);
dragNotifySubscribers();
}
public void clearPoints(){
points.clear();
dragNotifySubscribers();
}
}
2.通过算法判断圆是否被圈中
算法思路:
首先排除经过的点有经过圆的。
然后将圆心放在坐标轴中心,四个象限必须全部有经过的点才算作被包裹。(可以使用的大多数情况)
但是也有例外。就是经过圆四周但是不包裹圆的情况,这个属于算法上的缺陷
如上图,就无法判定圆不在索套中
public boolean isBlobInPoints(Blob blob){
List<Integer> list = new ArrayList<>();
List<Point2D> points = iModel.getPoints();
for (Point2D point : points) {
if (getLength(blob,point) < blob.getR()) {
return false;
}
if (point.getX() < blob.getX() && point.getY() < blob.getY()){
if (list.size() == 0){
list.add(1);
}
if (list.size() != 0 && list.get(list.size() - 1) != 1){
list.add(1);
}
}
if (point.getX() > blob.getX() && point.getY() < blob.getY()){
if (list.size() == 0){
list.add(2);
}
if (list.size() != 0 && list.get(list.size() - 1) != 2){
list.add(2);
}
}
if (point.getX() > blob.getX() && point.getY() > blob.getY()){
if (list.size() == 0){
list.add(3);
}
if (list.size() != 0 && list.get(list.size() - 1) != 3){
list.add(3);
}
}
if (point.getX() < blob.getX() && point.getY() > blob.getY()){
if (list.size() == 0){
list.add(4);
}
if (list.size() != 0 && list.get(list.size() - 1) != 4){
list.add(4);
}
}
}
if (list.size() >= 4 && list.contains(1) && list.contains(2) && list.contains(3) && list.contains(4)){
return true;
}
return false;
}
3.在controller类中拖动时将点记录在iModel中
public class BlobController {
public void handlePressed(MouseEvent event) {
switch (currentState) {
case READY -> {
if (model.hitBlob(event.getX(),event.getY())) { // 选中⚪
Blob b = model.whichHit(event.getX(),event.getY());
if (event.isControlDown()){
if (iModel.containedInSelected(b)){
iModel.deleteSelectedBlob(b);
}else {
iModel.addSelectedBlobs(b);
iModel.unselect();
}
}else if (iModel.getSelectedBlobs().isEmpty()){
iModel.setSelected(b);
}
prevX = event.getX();
prevY = event.getY();
currentState = State.DRAGGING; // 选中之后就改变状态为dragging
} else {
if (event.isControlDown()){
iModel.clearPoints();
// 按压时将pathComplete -> false
iModel.setPathComplete(false);
System.out.println("设置起点");
iModel.setInitialPointX(event.getX());
iModel.setInitialPointY(event.getY());
// 记录起始点
iModel.addPoint(new Point2D(event.getX(), event.getY()));
currentState = State.DRAGGING_TOOL;
} else {
currentState = State.PREPARE_CREATE; // 未选中 就意味着在释放时要创建新的⚪
}
}
}
}
}
public void handleDragged(MouseEvent event) { // 鼠标拖动
switch (currentState) {
case DRAGGING_TOOL -> {
if (event.isControlDown()){
iModel.clearSelectedBlobs();
System.out.println("设置游标点");
iModel.setCursorXY(event.getX(), event.getY());
// 添加拖动时的点
iModel.addPoint(new Point2D(event.getX(), event.getY()));
System.out.println("起点为"+iModel.getInitialPointX()+","+iModel.getInitialPointY());
System.out.println("游标点:"+iModel.getCursorX()+","+iModel.getCursorY());
model.getBlobs().forEach(b -> {
if (isBlobInStrokeRect(b)){
iModel.addSelectedBlobs(b);
}
// 判断是否被套中
if (isBlobInPoints(b)){
iModel.addSelectedBlobs(b);
}
});
}
}
}
}
}
4.将imodel记录的点在view层显示出来
public class BlobView extends StackPane implements BlobModelListener, IModelListener {
private void drawDrag() {
// 矩形
if (!iModel.isPathComplete()){
gc.setStroke(Color.GREEN);
gc.strokeRect(Math.min(iModel.getInitialPointX(),iModel.getCursorX()),
Math.min(iModel.getInitialPointY(),iModel.getCursorY()),
Math.abs(iModel.getCursorX() - iModel.getInitialPointX()),
Math.abs(iModel.getCursorY() - iModel.getInitialPointY()));
}
// 橡皮筋
if (!iModel.isPathComplete()) {
gc.setFill(Color.DARKGRAY);
iModel.getPoints().forEach(p -> gc.fillOval(p.getX()-3,p.getY()-3,6,6));
}
}
}
3.最后对part2,part3中的撤销重做,剪切板等功能进行完善,实现对多选之后的操作的适配
思路:将撤销重做栈 和 剪切板 修改为列表数据类型即可,之前的所有操作都修改为基于列表的操作即可。就是个体力活,不多介绍啦。