点云的点选、框选事件,现在需要这样的操作:如果要在屏幕上框选点云,需要先点击屏幕、按下键盘x键出发框选,用鼠标框选、按下键盘x键表示结束框选、按下q键表示退出交互(到这一步的实现参考这篇)。这种键盘+鼠标的操作模式使用上确实太麻烦,所以这一篇在前一篇的基础上,用模拟键盘、鼠标消息的方法,代替了键盘+鼠标的交互模式。方法很野路子,有好办法的还请指教。
pcl1.8(vtk等等都是all in one直接安装的版本),vs2015
点选和框选是通过调用pcl的registerPointPickingCallback()、registerAreaPickingCallback()来完成的。而pcl定义了自己的vtk交互风格,制定了一些按键来触发一些动作,这些动作包括点选和框选。具体的按键说明:
根据需要,这里模拟了x、q和shift+left click键盘。
在PCL点选、框选(封装为类)记录的方法中,无论是按下x还是q,都需要在英文输入发状态下发送。所以在代码中,要切换为英文输入法
HKL hCurKL;
hCurKL = GetKeyboardLayout(0);
LoadKeyboardLayout("0x0409", KLF_ACTIVATE); //这里会切换为美式键盘的英文输入法
在PCL点选、框选(封装为类)记录的方法中,每注册一次回调函数(registerPointPickingCallback()、registerAreaPickingCallback()),他们都入栈等待一个“q”键按下的事件,每敲一次q,就退出最后开启的那一个交互。我希望每次结束都一次性结束所有,所以添加了一个变量记录已经开启了多少个交互。
.h文件
int qNum = 0;
int curInteractionNum();
在PCL点选、框选(封装为类)记录的方法中,第一次按下x1之后可以开始框选,第二次按下x表示结束框选。这里把第一次按下x,记为x1,第二次记为x2.
::keybd_event(0x58, 0, 0, 0);
Sleep(20);
::keybd_event(0x58, 0, KEYEVENTF_KEYUP, 0);
keybdStyle = true;
在PCL点选、框选(封装为类)记录的方法中,x2表示结束框选,按下q表示退出交互
::keybd_event(0x58, 0, 0, 0);
Sleep(20);
::keybd_event(0x58, 0, KEYEVENTF_KEYUP, 0);
Sleep(20);
::keybd_event(0x51, 0, 0, 0);
Sleep(20);
::keybd_event(0x51, 0, KEYEVENTF_KEYUP, 0);
keybdStyle = false;
主要流程:移动鼠标到视口区域,点击,移动鼠标回原位。以下代码下载一个按钮的点击事件中。
POINT lpPoint;//用来记录控件的中心点,将模拟鼠标点击这个点
POINT oldlpPoint;//记录原本所在的点,模拟了单击后,光标回到原点
CRect rc;
CWnd* pParent = AfxGetApp()->GetMainWnd();
GetDlgItem(IDC_PCLVIEW)->GetWindowRect(&rc); 获取IDC_PCLVIEW的屏幕坐标
lpPoint.x = (rc.right + rc.left) / 2;
lpPoint.y = (rc.bottom + rc.top) / 2;
SetCursorPos(lpPoint.x, lpPoint.y);
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP | MOUSEEVENTF_ABSOLUTE, 0, 0, 0, 0);
Sleep(20);
SetCursorPos(oldlpPoint.x, oldlpPoint.y);//光标回到原点
GetDlgItem()、SetCursorPos()、mouse_event()都是windows API,前两个涉及的坐标都是屏幕坐标,最后的mouse_event在设置MOUSEEVENTF_ABSOLUTE的情况下也是用的绝对坐标。(关于坐标的基本概念:windows的屏幕左上点为基点,也即原点,向右x轴正向,向下y轴正向。顺便区分两个概念 ,屏幕坐标和窗口坐标。屏幕-左上点:屏幕(显示器)的左上点 就是 (0,0)。窗口-左上点:指我们所打开程序窗口的客户区左上点)
如果不模拟点击事件,仅仅发送键盘事件是无效的。原因猜测如下:
首先,真正用于显示和交互的控件(也就是视口),是这两个成员变量:
vtkSmartPointer m_win;
vtkSmartPointer m_iren;
那么先了解这两个成员变量怎么起作用的:vtkRenderWindow(窗口):初始化中通过窗口句柄绑定了一个控件,vtkRenderWindow本身则通过vtkRenderWindowInteractor的SetRenderWindow()方法绑定了一个vtkRenderWindowInteractor(窗口交互器)。这个vtkRenderWindowInteractor用于获取渲染窗口上发生的鼠标,键盘,事件事件,提供了独立于平台的与渲染窗口进行交互的机制,包括picking 和帧速率控制。当vtkRenderWindowInteractor(事实上是他的一个子类)观察到平台的某个事件发生时,他就通过InvokeEvent()方法把该事件转换为VTK事件。该类作为某些具体平台的基类来控制鼠标/键盘/时间消息的传送,通知vtkInteractorObserver和它的子类。注册于该交互器的所有观察者对象vtkInteractorObservers 都会接受到该事件,然后都对该事件进行响应。参考链接)
所以我判断,仅发送键盘事件是不起作用,原因就在这两个成员变量身上。假设vtkRenderWindow在被激活的状态下,才会接收到消息。但是为了触发按键模拟,我都得先我点击一个按钮。那么焦点就移到了按钮上,也就是消息发送不到vtkRenderWindow上。为此,在点击事件的代码中,我加了一行setfocus,将焦点设置到vtkRenderWindow所在的控件,但并没有起作用。我猜测是vtkRenderWindow本身其实并没有被设置成焦点。所以这时候直接模拟键盘消息,无论是sendmessage发消息到控件,还是keybd_event模拟键盘down和up,vtkRenderWindowInteractor反正都没有截取到这些事件(x键按下、q键按下等等)。
所以无奈之下,我就模拟了点击控件范围内一个位置的鼠标事件,这才使得vtkRenderWindow“被激活”。
由于x2、q键的发送,都需要先点击控件中心点以激活vtk视口的交互,所以只写在析构函数是没有用的,只能由在调用者去控制结束。因此我在调用者的类里添加了KCFinish()结束所有消息(正常流程下在某个按钮点击事件里面调用KCFinish(),但为了应对用户直接叉掉对话框,在OnClose消息里面也加了KCFinish()调用。
//结束所有的交互
//包括Shift键弹起
//如果需要发送X2和q则发送
void CHS_CtrlDlg::KCFinish()
{
m_PCLView_KC.shiftUp();
setFocusToVTK();
m_PCLView_KC.simulateEnding();
}
void CHS_CtrlDlg::OnClose()
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
if (m_PCLView_KC.qNum > 0)
KCFinish();
else
CDialogEx::OnClose();
}
参考:Qt之Windows下禁用和启用中文输入法