最近一直在做沙箱项目,在项目快接近结尾的时候,我想给在我们沙箱中运行的程序界面打上一个标记——标识其在我们沙箱中运行的。我大致想法是:在被注入程序的顶层窗口上方显示一个“标题性”窗口,顶层窗口外框外显示一个“异形”的空心窗口。这些窗口如影子般随着其被“吸附”窗口移动而移动,大小变化而变化。(转载请指明出处)以记事本为被注入程序为例:

我用的注入和HooKApi方案是采用微软的detour库。关于如何HookApi的方法,可以参看我之前的《一种注册表沙箱的思路、实现——Hook Nt函数》。注入的方案,我采用的Detour库的DetourCreateProcessWithDll函数,该函数的W版原型是

BOOL WINAPI DetourCreateProcessWithDllW(LPCWSTR lpApplicationName,__in_z LPWSTR lpCommandLine,LPSECURITY_ATTRIBUTES lpProcessAttributes,LPSECURITY_ATTRIBUTES lpThreadAttributes,BOOL bInheritHandles,DWORD dwCreationFlags,LPVOID lpEnvironment,LPCWSTR lpCurrentDirectory,LPSTARTUPINFOW lpStartupInfo,LPPROCESS_INFORMATION lpProcessInformation,LPCSTR lpDllName,PDETOUR_CREATE_PROCESS_ROUTINEW pfCreateProcessW)

该函数从倒数第二个参数之前的全是CreateProcessW的参数,倒数第二个参数是我们要注入的DLL的路径,最后一个参数是真实的CreateProcessW的函数入口地址。该函数的实现细节是:

1 以挂起的方式启动被注入程序

2 在内存中,修改被注入程序的导入表信息,在表中增加一个我们要注入的DLL中的导出函数

3 恢复被挂起的进程

该方案通过修改程序导入表,让系统误以为该程序需要调用到我们要注入的DLL中的导出函数,于是将我们注入的DLL加载到该进程内存空间,从而实现注入。这儿有个细节要说明:该方案要求我们注入DLL要至少有一个导出函数,哪怕这个函数什么也不做。

源码中RegSandBoxMainDialog工程是个MFC工程,它用于启动我们注入的进程并实现注入。我们查看其注入代码的实现

    UpdateData(TRUE);char chCurrentPath[MAX_PATH] = {0};GetModuleFileNameA( NULL, chCurrentPath, MAX_PATH );std::string wszCurrentPath = chCurrentPath;size_t nindex = wszCurrentPath.rfind('\\');wszCurrentPath = wszCurrentPath.substr( 0, nindex );wszCurrentPath += "\\";std::string szDllPath = wszCurrentPath;szDllPath += "HookWindow.dll";     // 拼接处注入DLL的完整路径std::wstring wszFilePath = m_Path; // 被注入进程的路径STARTUPINFO StartupInfo;ZeroMemory(&StartupInfo, sizeof(STARTUPINFO));StartupInfo.cb = sizeof(STARTUPINFO);PROCESS_INFORMATION ProcessInfo;ZeroMemory(&ProcessInfo, sizeof(PROCESS_INFORMATION));// FL:使用DetourCreateProcessWithDll需要注入的DLL要有一个导出函数BOOL bSuc = DetourCreateProcessWithDll( wszFilePath.c_str(), NULL, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE ,NULL, NULL, &StartupInfo, &ProcessInfo, szDllPath.c_str(), CreateProcessW );

HookWindow工程编译连接处HookWIndow.dll,我将在之后一步一步介绍这个DLL的编写。

HookWindow是一个Win32 dll工程,我们为其定义一个def文件HookWindow.def,其内容为:

LIBRARY  "HookWindow"
EXPORTS Notify

Notify函数是为了达到DetourCreateProcessWithDll要求:注入DLL必须要至少有一个导出函数(原因已在上面说明过)而设计的,实际这个函数什么也没做,他就是个空壳。

VOID Notify(){
}

现在得开始考虑窗口的实现了。我们知道windows系统是消息驱动的模型,那么我们的“吸附”窗口的消息模型该是什么样的?当时我思考方案时得出以下两种方案:

1 Hook进程内窗口消息,在消息链中根据顶层窗口消息而决定我们窗口的创建、显示、隐藏和销毁。这相当于我们窗口的消息循环使用了被注入进程的顶层窗口的消息循环。

2 注入进程后,启动一个线程,该线程负责创建窗口,同时在该线程中再启动一个监视被注入进程顶层窗口的线程,该线程将根据其得到的被注入进程窗口的位置大小状态等信息告诉我们窗口应该做何种处理。
       这两种方法各有其优缺点,方法1比方法2少1个线程,但是存在一种场景:当点击被注入程序顶层窗口的非客户区时,我们的窗口会被盖掉,因为这个时候还没轮到我们窗口处理该消息(SetWIndowsHookEx WH_CALLWNDPROCRET),此时我们无法让我们窗口显示在被注入进程顶层窗口前面。方法2就是比方法1多出线程数,如果我想创建两个窗口,就多出两个窗口线程,以此类推。如我设想的需求,我将创建一个管理外框异形空心窗口的线程和一个“标题”窗口,那就多出两个线程。

我觉得我这两个窗口要处理的消息非常简单,同样也想做点与众不同。于是我设计了这样的方案,方案是融合了方案1和方案2的优点:

SetWindowsHookEx勾住被注入进程的消息,同时设置Hook类型为WH_CALLWNDPROCRET。

VOID HookWindowsFn()
{do {g_hhook = SetWindowsHookEx( WH_CALLWNDPROCRET, CallWndRetProc, NULL, GetCurrentThreadId() );if ( NULL == g_hhook ) {_ASSERT(FALSE);}InitializeCriticalSection( &g_cs );} while (0);
}

这样我们将在原程序处理完消息后进行消息处理。

LRESULT CALLBACK CallWndRetProc( __in int nCode, __in WPARAM wParam, __in LPARAM lParam )
{if ( NULL != lParam ) {LPCWPRETSTRUCT lptagCWPRETSTRUCT = (LPCWPRETSTRUCT)lParam;DealMsg( lptagCWPRETSTRUCT->hwnd, lptagCWPRETSTRUCT->message, lptagCWPRETSTRUCT->wParam, lptagCWPRETSTRUCT->lParam );}return CallNextHookEx(NULL, nCode, wParam, lParam);
}

当我们收到消息时,我们要判断是否是我们关心的消息,这样将减少我们处理消息的线程的工作量。

BOOL IsNeedDealMsg( UINT uMsg )
{return ( IsNeedShowMsg( uMsg ) || ( WM_DESTROY == uMsg )|| ( WM_CLOSE == uMsg ) );
}BOOL IsNeedShowMsg( UINT uMsg )
{return ( ( WM_SHOWWINDOW == uMsg )|| ( WM_MOVE == uMsg )|| ( WM_MOVING == uMsg )|| ( WM_SIZE == uMsg )|| ( WM_WINDOWPOSCHANGED == uMsg ) );
}
VOID DealMsg( HWND hAttachedWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{if ( FALSE == IsNeedDealMsg( uMsg ) ) {return;}

其次判断该窗口是否为我们自己创建的“吸附”窗口。如果是我们的“吸附”窗口,我们将不会做任何处理。

VOID DealMsg( HWND hAttachedWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{if ( IsHelperWindow( hAttachedWnd ) ) {return;}
BOOL IsHelperWindow( HWND hwnd )
{WCHAR wszClassNameBuffer[MAX_PATH] = {0};int nClassNameLength = GetClassName( hwnd , wszClassNameBuffer, MAX_PATH - 1 );if ( 0 != nClassNameLength ) {std::wstring wszClassName = wszClassNameBuffer;if ( 0 == wcscmp( wszClassNameBuffer, TITILEWINDOWCLASS ) ||0 == wcscmp( wszClassNameBuffer, OUTSIDEWINDOWCLASS ) ) {return TRUE; // 通过类名判断}}return FALSE;
}

然后我们需要根据消息类型,对窗口句柄做个判断。因为如果我们“宿主”窗口处理完WM_DESTROY后,我们再将不能对其调用GetWindowLong以获取其样式。于是对WM_DESTORY消息,我们只是判断其是否为顶层窗口。如果不是该消息,我们将判断该窗口是否为顶层窗口,且其窗口样式包含WS_SYSMENU(我试验了下,我所遇到的我认为该处理的窗口都有该属性,这个属于经验之谈,不一定准确)。

if ( WM_DESTROY != uMsg ) {if ( FALSE == IsValibleWindow( hAttachedWnd ) )  {return;}}else {if ( FALSE == IsBaseWindow(hAttachedWnd) ) {return;}}if ( FALSE == IsNeedDealMsg( uMsg ) ) {return;}
BOOL IsValibleWindow( HWND hWnd )
{if ( FALSE == IsBaseWindow( hWnd ) ) {return FALSE;}DWORD dwStyle = ::GetWindowLong( hWnd, GWL_STYLE );if ( !( WS_SYSMENU & dwStyle ) ) {return FALSE;}return TRUE;
}BOOL IsTopWindow( HWND hwnd )
{BOOL bTop = FALSE;do {HWND hParentHwnd = NULL;HWND hParenthwnd = GetParent( hwnd );if ( NULL == hParenthwnd ){bTop = TRUE;}} while (0);return bTop;
}BOOL IsBaseWindow( HWND hWnd )
{if ( FALSE == ::IsWindow(hWnd) || FALSE == IsTopWindow(hWnd) ) {return FALSE;}return TRUE;
}

如果是原程序创建的窗口,则判断该句柄是否已经存在一个管理“吸附”窗口的线程(该信息保存在一个Map中)。如果不存在,就创建一个管理两个“吸附”窗口的线程,并将<HWND,HTHREADHANDLE>对保存到Map中。如果存在,则向这个线程管理的窗口发送相应的消息。一个进程可能不止是存在一个顶层窗口,所以我这儿要建立Map信息。

typedef struct _WindowThreadColloction_{LPCWindowThread lpTitleWindowThread;
}WindowThreadColloction, *pWindowThreadColloction;typedef std::map<HWND,WindowThreadColloction> MapHwndThread;
typedef MapHwndThread::iterator MapHwndThreadIter;
    LPCWindowThread lpWindowThread = NULL;lpWindowThread = GetTitleWindowThread( hAttachedWnd );if ( NULL == lpWindowThread ) {if ( WM_SHOWWINDOW == uMsg ) {lpWindowThread = new CWindowThread(hAttachedWnd);if ( NULL == lpWindowThread ) {_ASSERT(FALSE);return;}UpdateHwndTitleWindowThread( hAttachedWnd, lpWindowThread );}else {return;}}lpWindowThread->NotifyMsg( uMsg );
}

现在我们将看一下我们管理两个“吸附”窗口的线程类。

class CWindowThread:   public CMessageLoop,public CMessageFilter
{
public:CWindowThread(void);~CWindowThread(void);
public:CWindowThread(HWND hAttachWindow);
public:VOID NotifyMsg(UINT uMsg);VOID ExitThread();BOOL PreTranslateMessage(MSG* pMsg);
private:static DWORD WINAPI ThreadRoutine(LPVOID lpParam);
private:HWND m_hAttachWindow;HANDLE m_hThread;CWTLTitleWindow* m_pCWTLTitleWindow;CWTLOutSideWindow* m_pCWTLOutSideWindow;
};
typedef CWindowThread* LPCWindowThread;

消息循环是在该线程中的,于是继承于CMessageLoop;因为我们要让我们窗口屏蔽ATL+F4这类的操作,所以我们要PreTranslateMessage,于是要继承于CMessageFilter。

BOOL CWindowThread::PreTranslateMessage( MSG* pMsg )
{if ( WM_SYSKEYDOWN == pMsg->message ) {return TRUE;}return FALSE;
}
DWORD WINAPI CWindowThread::ThreadRoutine( LPVOID lpParam )
{CWindowThread* pThis = (CWindowThread*) lpParam;if ( NULL == pThis ){return 0xFFFFFFFF;}if ( NULL == pThis->m_pCWTLTitleWindow ) {pThis->m_pCWTLTitleWindow = new CWTLTitleWindow( pThis->m_hAttachWindow );pThis->m_pCWTLTitleWindow->Create( pThis->m_hAttachWindow ); // 注意这儿要设置为父窗口pThis->m_pCWTLTitleWindow->RunWindowMsgLoop();}if ( NULL == pThis->m_pCWTLOutSideWindow ) {pThis->m_pCWTLOutSideWindow = new CWTLOutSideWindow( pThis->m_hAttachWindow );pThis->m_pCWTLOutSideWindow->Create( pThis->m_hAttachWindow ); // 注意这儿要设置为父窗口pThis->m_pCWTLOutSideWindow->RunWindowMsgLoop();}pThis->Run(); // 启动消息循环return 0;
}
VOID CWindowThread::NotifyMsg( UINT uMsg )
{if ( m_pCWTLTitleWindow ){m_pCWTLTitleWindow->DealMsg( uMsg );}if ( m_pCWTLOutSideWindow ) {m_pCWTLOutSideWindow->DealMsg( uMsg );}
}

下面再来看看窗口的实现。因为我们要做的是“吸附”窗口,该窗口应该不能影响原窗口正常的行为(比如不应该抢焦点,不在任务栏出现),同时考虑到刷新问题,我们要让该窗口具有双缓存。以“标题”窗口为例

class CWTLTitleWindow:public CDoubleBufferWindowImpl< CWTLTitleWindow, CWindow, CWinTraits<WS_POPUP|WS_CLIPSIBLINGS, WS_EX_LEFT|WS_EX_LTRREADING|WS_EX_NOPARENTNOTIFY|WS_EX_NOACTIVATE>>
{
public:typedef CWTLTitleWindow _thisClass;typedef CDoubleBufferImpl<_thisClass> _baseDblBufImpl;DECLARE_WND_CLASS_EX(TITILEWINDOWCLASS, CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS, COLOR_WINDOW);CWTLTitleWindow(void);~CWTLTitleWindow(void);
public:CWTLTitleWindow(HWND hAttachWindow);BEGIN_MSG_MAP_EX(CWTLTitleWindow)MESSAGE_HANDLER( WM_SHOWWINDOW, OnShow )MESSAGE_HANDLER( WM_DESTROY, OnDestroy)MESSAGE_HANDLER( WM_QUIT, OnQuit )MESSAGE_HANDLER( WM_MOUSEACTIVATE, OnMouseActive )MESSAGE_RANGE_HANDLER( WM_USER, WM_USER + WM_USER, OnDealUserMsg )CHAIN_MSG_MAP(_baseDblBufImpl)END_MSG_MAP()LRESULT OnShow(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);LRESULT OnQuit(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);LRESULT OnMouseActive(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);LRESULT OnDealUserMsg(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);void DoPaint(HDC dc);VOID Start();VOID DealMsg( UINT uMsg );private:ECalcResult CalcTitleWindowXY( int& x, int& y );VOID ShowWindow();
private:HWND m_hAttachHWnd;HINSTANCE m_hInstance;HBITMAP m_hBitmap;RECT m_hAttachWindowRect;
}; 

首先说一下双缓冲。我继承于CDoubleBufferWindowImpl。在消息映射中,我们要让我们不处理的消息交给基类处理

    typedef CWTLTitleWindow _thisClass;typedef CDoubleBufferImpl<_thisClass> _baseDblBufImpl;
CHAIN_MSG_MAP(_baseDblBufImpl)

同时实现DoPaint函数。

void CWTLTitleWindow::DoPaint( HDC dc )
{CRect rc;GetClientRect(&rc);    CMemoryDC MemDc( dc, rc );HBRUSH hBitmapBrush = CreatePatternBrush(m_hBitmap);if ( NULL == hBitmapBrush ) {return;}MemDc.FillRect( &rc, hBitmapBrush );DeleteObject( hBitmapBrush );
}

m_bBitmap是我在资源文件中的一个bmp图片,我们在Start函数中将其载入。在类释放时,将其delete。

CWTLTitleWindow::~CWTLTitleWindow(void)
{if ( NULL != m_hBitmap ) {DeleteObject( m_hBitmap );}
}VOID CWTLTitleWindow::Start()
{
#pragma warning(push)
#pragma warning(disable:4312)if ( NULL == m_hInstance ) {m_hInstance = (HINSTANCE)GetWindowLong( GWL_HINSTANCE );}
#pragma warning(pop)if ( NULL == m_hBitmap ) {m_hBitmap = LoadBitmap( m_hInstance, MAKEINTRESOURCE(IDB_BITMAP1));}
}

以上基本上算是完成了双缓冲的操作了,但是为了尽量减少刷新的次数,我会多加个判断:改变的位置和大小是否和现在的位置和大小一致,如果一致则不做任何操作,否则刷新。

ECalcResult CWTLTitleWindow::CalcTitleWindowXY(int& x, int& y )
{ECalcResult eResult = EError;do {RECT rcAttachWindow;if ( FALSE == ::GetWindowRect( m_hAttachHWnd, &rcAttachWindow ) ) {break;}if ( rcAttachWindow.left == m_hAttachWindowRect.left && rcAttachWindow.right == m_hAttachWindowRect.right&& rcAttachWindow.top == m_hAttachWindowRect.top && rcAttachWindow.bottom == m_hAttachWindowRect.bottom ){eResult = ENoChange;break;}else {m_hAttachWindowRect = rcAttachWindow;}x = ( rcAttachWindow.left + rcAttachWindow.right - TIPWINDTH ) / 2;y = rcAttachWindow.top;eResult = ESuc;} while (0);return eResult;
}

再说下无焦点窗口的细节。

首先窗口样式要有WS_POPUP,网上有人说还要加上WS_VISIBLE,但是我觉得没必要。其次扩展属性要有WS_EX_NOACTIVATE。再次我们要处理WM_MOUSEACTIVATE消息。

MESSAGE_HANDLER( WM_MOUSEACTIVATE, OnMouseActive )
LRESULT CWTLTitleWindow::OnMouseActive( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{return MA_NOACTIVATE; // MA_NOACTIVATEANDEAT亦可
}

最后要特别注意下窗口显示和移动对焦点的影响。在窗口显示时,如果我们使用ShowWindow和MoveWindow这类的函数,会导致我们我们窗口还可以获得焦点。我们要使用SetWindowPos,最后一个参数要带上SWP_NOACTIVATE。

VOID CWTLTitleWindow::ShowWindow()
{if ( FALSE == IsBaseWindow( m_hAttachHWnd ) ) {return;}int x = 0; int y = 0;ECalcResult eResult = CalcTitleWindowXY( x, y );if ( EError == eResult ) {::SetWindowPos( m_hWnd, NULL, x, y, TIPWINDTH, TIPHEIGHT, SWP_NOACTIVATE | SWP_HIDEWINDOW );return;}else if ( ENoChange == eResult ) {return;}::SetWindowPos( m_hWnd, NULL, x, y, TIPWINDTH, TIPHEIGHT, SWP_NOACTIVATE | SWP_SHOWWINDOW);
}

最后说一下业务相关的消息传递。在被注入进程的顶层窗口接受到一些消息后,我们会将这些消息传递给我们的窗口,让其做一些处理。为了区分消息来源于顶层窗口还是自己,我将顶层窗口消息处理为一个用户自定义消息。

VOID CWTLTitleWindow::DealMsg( UINT uMsg )
{::PostMessage( m_hWnd, WM_USER + uMsg, NULL, NULL );
}

消息映射是这么写的,用于处理整个用户自定义消息(而不会处理顶层窗口传来的其用户自定义消息)

MESSAGE_RANGE_HANDLER( WM_USER, WM_USER + WM_USER, OnDealUserMsg )
LRESULT CWTLTitleWindow::OnDealUserMsg( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{UINT uAttachedWindowMsg = uMsg - WM_USER;if ( IsNeedShowMsg(uAttachedWindowMsg) ) {ShowWindow();}else {::PostMessage( m_hWnd, uAttachedWindowMsg, wParam, lParam );}return 1;
}

外框窗口和标题窗口基本类似,但是其背景是使用画笔画的,而不是通过贴图。另一个很大的区别就是外框窗口是一个空心的异形窗口。这些区别的主要体现是在DoPaint函数中

void CWTLOutSideWindow::DoPaint( HDC dc )
{CRect rc;GetClientRect(&rc);CMemoryDC MemDc( dc, rc );HRGN RgnInside = CreateRectRgn( rc.left + WIDTHHEIGHTADD, rc.top + WIDTHHEIGHTADD,rc.right - WIDTHHEIGHTADD, rc.bottom - WIDTHHEIGHTADD );HRGN RgnOut = CreateRectRgn( rc.left, rc.top, rc.right, rc.bottom );CombineRgn( RgnOut, RgnOut, RgnInside, RGN_DIFF );MemDc.FillRgn( RgnOut, m_brush );SetWindowRgn( RgnOut, TRUE ); // 设置异形窗口DeleteObject( RgnInside );DeleteObject( RgnOut );
}

这样整个工程OK。以下是源码工程。

源码工程

由于在国内最近不能访问OneDriver,故提供百度云盘的下载链接:http://pan.baidu.com/s/1i39sfdF 密码:rtfa

一种在注入进程中使用WTL创建无焦点不在任务栏出现“吸附”窗口的方法和思路相关推荐

  1. 打开方式中选择默认方式无反映_Win7系统无法选择打开方式的解决方法

    习惯用win7系统的用户在使用过程中一定会遇到这个问题:有的时候想要打开PDF文件,如果不安装其他软件,单用默认的打开方式是打不开的,安装了软件之后,仍然找不到自己想要用的打开方式. 今天小编以打开P ...

  2. Android中设置EditText默认无焦点

    在activity中放置了1个或1个以上的EditText,进入该activity的时候第一个EditText会接收焦点,我希望里面所有的EditText默认是不接收焦点的,该怎么做呢? 方法: 在第 ...

  3. python统计英文文章中单词的个数无文件_求Python统计英文文件内单词个数的思路...

    感谢微博上@刘鑫-MarsLiu的TAG每天一个小程序. 你会如何实现上述题目的要求? #!/usr/bin/env python # -*- coding: utf-8 -*- "&quo ...

  4. Pycharm 中选择conda创建的虚拟环境,但是不显示包显示的解决方法

    选择自己的虚拟环境后,点击下图所示:

  5. VC下提前注入进程的一些方法2——远线程带参数

    在前一节中介绍了通过远线程不带参数的方式提前注入进程,现在介绍种远线程携带参数的方法.(转载请指明出处) 1.2 执行注入的进程需要传信息给被注入进程 因为同样采用的是远线程注入,所以大致的思路是一样 ...

  6. VC下提前注入进程的一些方法1——远线程不带参数

    前些天一直在研究Ring3层的提前注入问题.所谓提前注入,就是在程序代码逻辑还没执行前就注入,这样做一般用于Hook API.(转载请指明出处)自己写了个demo,在此记下. 我的demo使用了两种注 ...

  7. 【安全技术】关于几种dll注入方式的学习

    何为dll注入 DLL注入技术,一般来讲是向一个正在运行的进程插入/注入代码的过程.我们注入的代码以动态链接库(DLL)的形式存在.DLL文件在运行时将按需加载(类似于UNIX系统中的共享库(shar ...

  8. Python中的多进程创建和传值(克隆)Queue方法

    今日怼人金句"OMG,你何时有这样高尚的想法了"(一般怼和你谈道德.良心的人) 先说什么是进程,进程按照笔者的理解就是空间+任务不对记得指正笔者,比如有一个公司(进程)里有员工在做 ...

  9. Hook技术之4 在自己的进程中注入一个Dll到别人的进程

    与其说是一种技术,不如说是一种技术思想.它使用了前面讲的那些Hook手段,来达到自己的目的. 其应用环境是这样的,自己有一个进程在运行,这个进程是自己可以控制的,但由于业务的需要,自己又要执行另一个应 ...

最新文章

  1. redis cluster 添加 删除 重分配 节点
  2. Selenium IDE工具界面剖析
  3. Linux7改运行级别,Centos7 修改运行级别
  4. 我可以/应该在事务上下文中使用并行流吗?
  5. “约见”面试官系列之常见面试题之第九十八篇之vue-router有哪几种导航钩子(建议收藏)
  6. css控制div等比高度
  7. 不用点击_网站推广怎么样才能提高点击量和转化率-西安青云在线
  8. vue方法传值到data_Vue组件创建和传值的方法
  9. 筛选DataTable数据的方法
  10. IDEA中如何使用debug调试项目 一步一步详细教程
  11. 计算机设备替换法,同义词替换表的挖掘方法及装置、电子设备、计算机可读介质与流程...
  12. linux目录更改权限不够,Linux中文件夹访问权限不足
  13. 二、进程管理(1.进程的基本概念)
  14. 转 Unity绳子插件Obi+Rope下载与简单使用方法
  15. MQTT-SN协议阅读之MQTT-SN vs MQTT
  16. Oracle中rowid的用法(全面)
  17. 停止抱怨的力量是多么强大!
  18. 网络多线程编程-简单实现(模拟QQ的实现)
  19. C#导出Word总结
  20. python 爬取微博实时热搜,并存入数据库实例

热门文章

  1. 物联网设备天线设计与选型指南
  2. 网络流最大流EK算法板子
  3. 【神经网络】(17) EfficientNet 代码复现,网络解析,附Tensorflow完整代码
  4. java数独中数独空格初始化,java高手近解决数独问题,看你是不是高手!
  5. python数据分析面试_python数据分析面试
  6. 2018目标检测最新算法+经典目标检测算法
  7. cordova版本更新_ionic4 APP版本更新
  8. 剑指offer: 面试题40. 最小的k个数
  9. Vue、angular等框架实现双向绑定的原理,核心机制是使用了Object.defineProperty
  10. IDEA、webstorm设置编辑器恶心的竖线位置、隐藏竖线(参考线),然后代码自动换行